Skip to main content

Router

Pashmak uses facade pattern to simplify access to core components. The Router facade is the main provider of available http routes to http server.

Basic Usage​

import { Request, Response } from "@devbro/pashmak/router";
import { router } from "@devbro/pashmak/facades";
import { CatController } from "@/app/controllers/CatController";
import { AnimalController } from "@/app/controllers/AnimalController";
import { loggerMiddleware, logResponseMiddleware } from "@/middlewares";

// Add global middleware (applies to all routes)
router().addGlobalMiddleware(loggerMiddleware);

// Add a functional route
router()
.addRoute(
["GET", "HEAD"],
"/api/v1/countries",
async (req: Request, res: Response) => {
return { message: "GET countries" };
},
)
.addMiddleware(logResponseMiddleware);

// Register class-based controllers
router().addController(CatController);

// Nesting routers for better organization
let authnedRouter = new Router();
authnedRouter.addController(AnimalController);
authnedRouter.addGlobalMiddleware([authenticate]);
router().addRouter("/api/v1", authnedRouter);

Url structuring​

Pashmak requires valid url structure for routing. Url can be made of following parts:

  • Static parts: e.g. /api/v1/cats
  • Parameter parts: e.g. /api/v1/cats/:id where :id is a parameter part
  • Wildcard parts: e.g. /public/*filepath where *filepath matches all remaining parts of url

Valid name for parameter and wildcard parts are made of alphanumeric characters, and underscore(_). (regex: [a-zA-Z0-9_]+). The value of these parts can be accessed in controller via req.params.

URL Resolution​

For every incoming request, router will try to find the best matching route. If multiple routes match the url, the first registered route will be selected.

Controller Types​

The router supports two types of controllers:

  1. Functional Controllers: Simple async functions
  2. Class Controllers: Classes decorated with @Controller

Functional Controllers​

Basic format of a functional controller:

import { Request, Response } from "@devbro/pashmak/router";

async (req: Request, res: Response) => {
return { message: "GET regions" };
};

Controller class​

import { Controller, Get, Post, Put, Delete } from "@devbro/pashmak/router";
import { Request, Response } from "@devbro/pashmak/router";

@Controller("/api/v1/cats")
export class CatController {
@Get("/", { middlewares: [logResponseMiddleware] })
async getAllCats() {
let req = ctx().get<Request>("request");
let res = ctx().get<Response>("response");
return { message: "GET all cats" };
}

@Post("/")
async createCat() {
let req = ctx().get<Request>("request");
let res = ctx().get<Response>("response");
return { message: "Cat created" };
}

@Put("/:id")
async updateCat() {
let req = ctx().get<Request>("request");
let res = ctx().get<Response>("response");
const { id } = req.params;
return { message: `Cat ${id} updated` };
}

@Delete("/:id")
async deleteCat() {
let req = ctx().get<Request>("request");
let res = ctx().get<Response>("response");
const { id } = req.params;
return { message: `Cat ${id} deleted` };
}

@Get(":id/logs")
async getCatLogs() {
let req = ctx().get<Request>("request");
let res = ctx().get<Response>("response");
const { id } = req.params;
return { message: `Logs for cat ${id}` };
}
}

Direct Response Manipulation​

For more complex responses, you can directly modify the Response object:

Note: Response and Request objects are NOT the standard Node.js objects. Make sure to import them from @devbro/pashmak/router.

import { Request, Response } from "@devbro/pashmak/router";

async (req: Request, res: Response) => {
res.writeHead(418, { "Content-Type": "text/plain" });
res.write("Can you guess what I am?");
};

Accessing Request Data​

async (req: Request, res: Response) => {
const { id } = req.params; // URL parameters
const { search } = req.query; // Query string parameters
const data = req.body; // Request body
const files = req.files; // Uploaded files

return { id, search, data };
};

Error Handling​

The router handles errors automatically. If you throw an error of type HttpError, the HTTP server will render it as JSON. If the error is of any other type, the server will return a generic 500 error.

Built-in HTTP Errors​

import { Request, Response } from "@devbro/pashmak/router";
import {
HttpError,
HttpBadRequestError,
HttpUnauthorizedError,
HttpForbiddenError,
HttpNotFoundError,
HttpConflictError,
HttpInternalServerError,
} from "@devbro/pashmak/http";

async (req: Request, res: Response) => {
// Throw specific HTTP errors
throw new HttpUnauthorizedError("Please login first");
// or
throw new HttpNotFoundError("Resource not found");
// or
throw new HttpBadRequestError("Invalid input");
};

Custom Error Handler​

You can set a custom error handler for more control:

import { httpServer } from "@devbro/pashmak/facades";
import { httpServer } from "@devbro/pashmak/http";

httpServer().setErrorHandler(async (err: Error, req: any, res: any) => {
// Custom error handling logic
if (err instanceof CustomError) {
res.writeHead(err.statusCode, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "random error" }));
return;
}

handleHttpErrors(err, req, res); // optional: call default handler
});

Nesting Routers​

There will be situations where we need to simplify your routers using prefix or having middlewares that are applied to subset of routes. The simple solution is to create a new router and use addRouter to add it to main router.

import { router } from "@devbro/pashmak/facades";
import { Router } from "@devbro/pashmak/router"; // Capital Class Name

router().addRoute(
["GET", "HEAD"],
"/api/v1/meow",
async (req: any, res: any) => {
return { message: "meow meow!" };
},
);

let authnedRouter = new Router();

// add routes
authnedRouter.addController(OrganizationController);
authnedRouter.addController(UserController);

// add middlewares
authnedRouter.addGlobalMiddleware([authenticate]);

router().addRouter("/api/v1", authnedRouter);
// or
router().addRouter("", authnedRouter); // prefix can be empty string

Handling null/undefined in response​

When returning a json response, undefined is not considered a valid value. As a work around, all values of undefined will be converted to null during response.

Route Checks​

Route checks are predicate functions that gate whether a route (or an entire router) is eligible to handle a request. They are evaluated before the route handler runs, and if any check returns false the route is skipped — allowing the router to continue looking for the next matching candidate.

A RouteCheck has the following signature:

type RouteCheck = (req: Request, res: Response) => boolean;

Route-level checks​

Attach one or more checks directly to a route with addCheck(). All checks must pass for that route to be selected.

import { router } from "@devbro/pashmak/facades";
import { Request, Response } from "@devbro/pashmak/router";

// Two routes share the same URL but are differentiated by the Accept-Version header
router()
.addRoute(["GET"], "/api/users", async (req: Request, res: Response) => {
return { version: 1, users: [] };
})
.addCheck((req) => req.headers?.["accept-version"] === "1");

router()
.addRoute(["GET"], "/api/users", async (req: Request, res: Response) => {
return { version: 2, users: [] };
})
.addCheck((req) => req.headers?.["accept-version"] === "2");

A request with Accept-Version: 1 will be served by the first handler, and Accept-Version: 2 by the second. If no check passes, the router returns undefined (404).

Router-level checks​

Attach checks to a Router instance with addCheck(). These are evaluated first, before any individual route checks. If a router-level check fails, all routes in that router are skipped.

import { Router } from "@devbro/pashmak/router";
import { router } from "@devbro/pashmak/facades";

const v2Router = new Router();
v2Router.addCheck((req) => req.headers?.["accept-version"] === "2");

v2Router.addRoute(
["GET"],
"/api/users",
async (req: Request, res: Response) => {
return { version: 2, users: [] };
},
);

v2Router.addRoute(
["GET"],
"/api/orders",
async (req: Request, res: Response) => {
return { version: 2, orders: [] };
},
);

router().addRouter("/api", v2Router);

Any request that does not carry Accept-Version: 2 will bypass all routes in v2Router.

Combining router-level and route-level checks​

When a sub-router is mounted via addRouter(), its checks are propagated into each of its routes. This means a route inside a sub-router must satisfy both the router's checks and its own checks:

const subRouter = new Router();
subRouter.addCheck((req) => req.headers?.["accept-version"] === "2"); // router-level

subRouter
.addRoute(["GET"], "/api/feature", async (req: Request, res: Response) => {
return { feature: "enabled" };
})
.addCheck((req) => req.headers?.["x-feature"] === "enabled"); // route-level

router().addRouter("", subRouter);
// The /api/feature route requires BOTH accept-version: 2 AND x-feature: enabled

Multiple checks​

addCheck() accepts a single function or an array of functions. All checks are evaluated in order and all must return true:

route.addCheck([
(req) => req.headers?.["accept-version"] === "2",
(req) => req.headers?.["x-feature"] === "enabled",
]);

// same as
route.addCheck((req) => req.headers?.["accept-version"] === "2");
route.addCheck((req) => req.headers?.["x-feature"] === "enabled");