A simple yet powerful routing solution for Express.js
applications that provides a clean and intuitive way to organize routes and actions.
- Simple route definition syntax (similar to Rails framework)
- Action-based approach (similar to Hanami framework)
- Each action in a separate file for better maintainability
- Scoped routes for better organization
- Automatic action loading
- Development: docs/DEVELOPMENT.md
- Development (RU): docs/DEVELOPMENT.ru.md
- Demo app Skeleton: docs/demo/README.md
npm install @the-teacher/the-router
yarn add @the-teacher/the-router
pnpm add @the-teacher/the-router
routes/index.ts
import { root, get, post, routerScope as scope } from "@the-teacher/the-router";
// Define root route
root("index#index"); // Will use src/actions/index/indexAction.ts
// Define GET and POST routes
get("/users", "users#show"); // Will use src/actions/users/showAction.ts
post("/users", "users#create"); // Will use src/actions/users/createAction.ts
// Define GET and POST routes
scope("admin", () => {
get("/users", "users#create"); // Will use src/actions/admin/users/createAction.ts
post("/users", "users#update"); // Will use src/actions/admin/users/updateAction.ts
});
Each action is defined in its own file:
src/
actions/
index/
indexAction.ts # handles get "/"
users/
showAction.ts # handles get "/users"
createAction.ts # handles post "/users"
admin/
users/
createAction.ts # handles get "/admin/users"
updateAction.ts # handles post "/admin/users"
Action file example:
// src/actions/users/showAction.ts
import { Request, Response } from "express";
export const perform = (req: Request, res: Response) => {
res.json({ message: "Users list" });
};
Each action is defined in its own file and must export a perform
function:
// src/actions/users/showAction.ts
import { Request, Response } from "express";
// perform - required method for each action
export const perform = (req: Request, res: Response) => {
res.json({ message: "Users list" });
};
Group related routes under a common prefix:
scope("admin", () => {
get("/users", "admin/users#list"); // Will use src/actions/admin/users/listAction.ts
post("/users", "admin/users#create"); // Will use src/actions/admin/users/createAction.ts
});
You can add middleware to any route:
import { get, post, routeScope as scope } from "@the-teacher/the-router";
import { authenticate } from "./middlewares/auth";
import { validateUser } from "./middlewares/validation";
// Single middleware
get("/users/:id", [authenticate], "users#show");
// Multiple middleware in execution order
post("/users", [authenticate, validateUser], "users#create");
// Root route with middleware
root([authenticate], "index#index");
// Simple routes without middleware
get("/about", "pages#about");
post("/contact", "pages#contact");
// Middleware with scoped routes
scope("admin", [authenticate], () => {
// These routes inherit authentication from scope
get("/users", "users#show");
post("/users", "users#create");
// Additional middleware for specific routes
post("/users/:id", [validateUser], "users#update");
});
You can add middleware to both individual routes and entire scopes:
import { authenticate } from "./middlewares/auth";
import { validateUser } from "./middlewares/validation";
import { logRequest } from "./middlewares/logging";
// Apply middleware to all routes within scope
scope("admin", [authenticate], () => {
// These routes will require authentication
get("/users", "users#index");
post("/users", "users#create");
// This route will require both authentication and validation
post("/users/:id", [validateUser], "users#update");
});
// Combine multiple middleware for scope
scope("api", [authenticate, logRequest], () => {
get("/stats", "stats#index");
get("/health", "health#check");
});
// Simple scope without middleware
scope("public", () => {
get("/about", "pages#about");
get("/contact", "pages#contact");
});
Middleware specified as the second parameter will be applied to all routes within that scope.
You can still add route-specific middleware that will be executed after the scope middleware.
src/
index.ts
routes/
index.ts
actions/
index/
indexAction.ts
users/
showAction.ts
createAction.ts
updateAction.ts
posts/
showAction.ts
createAction.ts
admin/
users/
listAction.ts
createAction.ts
posts/
listAction.ts
updateAction.ts
Example of routes matching this structure:
import { root, get, post, routeScope as scope } from "@the-teacher/the-router";
// Root and basic routes
root("index#index"); // -> src/actions/index/indexAction.ts
get("/users", "users#show"); // -> src/actions/users/showAction.ts
post("/users", "users#create"); // -> src/actions/users/createAction.ts
get("/posts", "posts#show"); // -> src/actions/posts/showAction.ts
post("/posts", "posts#create"); // -> src/actions/posts/createAction.ts
// Admin scope
scope("admin", () => {
get("/users", "users#list"); // -> src/actions/admin/users/listAction.ts
post("/users", "users#create"); // -> src/actions/admin/users/createAction.ts
get("/posts", "posts#list"); // -> src/actions/admin/posts/listAction.ts
post("/posts", "posts#update"); // -> src/actions/admin/posts/updateAction.ts
});
This will create routes:
- GET
/
->src/actions/index/indexAction.ts
- GET
/users
->src/actions/users/showAction.ts
- POST
/users
->src/actions/users/createAction.ts
- GET
/posts
->src/actions/posts/showAction.ts
- POST
/posts
->src/actions/posts/createAction.ts
- GET
/admin/users
->src/actions/admin/users/listAction.ts
- POST
/admin/users
->src/actions/admin/users/createAction.ts
- GET
/admin/posts
->src/actions/admin/posts/listAction.ts
- POST
/admin/posts
->src/actions/admin/posts/updateAction.ts
Group related routes under a common prefix:
scope("admin", () => {
get("/users", "users#list"); // -> src/actions/admin/users/listAction.ts
post("/users", "users#create"); // -> src/actions/admin/users/createAction.ts
});
Routes can include dynamic parameters:
// Basic parameter routes
get("/users/:id", "users#show"); // -> /users/123
get("/posts/:id/comments", "posts#comments"); // -> /posts/456/comments
// Parameters with middleware
get("/users/:id", [authenticate], "users#show");
// Multiple parameters
get("/posts/:postId/comments/:commentId", "comments#show");
The order of route definitions matters. More specific routes should be defined before more general ones:
// ✅ Correct order
get("/posts/featured", "posts#featured"); // More specific route first
get("/posts/:id", "posts#show"); // General route second
// ❌ Wrong order - "/posts/featured" will never be reached
get("/posts/:id", "posts#show"); // General route catches all
get("/posts/featured", "posts#featured"); // Will never match
When using multiple middleware, it's recommended to group them in variables for better maintainability:
// Group related middleware
const authMiddlewares = [authenticate, checkRole];
const validationMiddlewares = [validateUser, sanitizeInput];
// Use middleware groups in routes
get("/users", authMiddlewares, "users#index");
post("/users", [...authMiddlewares, ...validationMiddlewares], "users#create");
// In scoped routes
const adminMiddlewares = [authenticate, requireAdmin, logAccess];
scope("admin", adminMiddlewares, () => {
get("/users", "users#list");
// Additional middleware for specific routes
const userUpdateMiddlewares = [validateUser];
post("/users/:id", userUpdateMiddlewares, "users#update");
});
Basic usage:
root(scopeAction)
: Define root route (/
)get(path, scopeAction)
: Define GET routepost(path, scopeAction)
: Define POST routeput(path, scopeAction)
: Define PUT routepatch(path, scopeAction)
: Define PATCH routedestroy(path, scopeAction)
: Define DELETE routeoptions(path, scopeAction)
: Define OPTIONS routehead(path, scopeAction)
: Define HEAD routeall(path, scopeAction)
: Define route for all HTTP methodsscope(prefix, callback)
: Group routes under a common prefix
With middleware:
root(middlewares[], scopeAction)
: Define root route with middlewareget(path, middlewares[], scopeAction)
: Define GET route with middlewarepost(path, middlewares[], scopeAction)
: Define POST route with middlewareput(path, middlewares[], scopeAction)
: Define PUT route with middlewarepatch(path, middlewares[], scopeAction)
: Define PATCH route with middlewaredestroy(path, middlewares[], scopeAction)
: Define DELETE route with middlewareoptions(path, middlewares[], scopeAction)
: Define OPTIONS route with middlewarehead(path, middlewares[], scopeAction)
: Define HEAD route with middlewareall(path, middlewares[], scopeAction)
: Define route for all HTTP methods with middlewarescope(prefix, middlewares[], callback)
: Group routes with middleware
Examples:
// Basic usage
root("index#index");
get("/users", "users#show");
post("/users", "users#create");
put("/users/:id", "users#update");
patch("/users/:id", "users#patch");
destroy("/users/:id", "users#delete");
options("/users", "users#options");
head("/users", "users#head");
all("/api", "api#handle");
scope("admin", () => {
/* routes */
});
// With middleware
const authMiddlewares = [authenticate, logRequest];
root([authenticate], "index#index");
get("/users", authMiddlewares, "users#show");
put("/users/:id", authMiddlewares, "users#update");
scope("admin", authMiddlewares, () => {
/* routes */
});
You can use regular expressions for route paths:
// Match paths ending with 'fly'
get(/.*fly$/, "insects#list"); // Matches: /butterfly, /dragonfly
get(/^\/api\/v\d+\/.*$/, "api#handle"); // Matches: /api/v1/users, /api/v2/posts
// RegExp routes with middleware
get(/^\/secure\/.*$/, [authenticate], "secure#handle");
// Order matters for RegExp routes too
get(/^\/api\/v1\/users$/, "users#list"); // More specific route first
get(/^\/api\/v1\/.*$/, "api#handle"); // General route second
Note: When using regular expressions, the path is passed to Express.js
as is, without any normalization.
Similar to Ruby on Rails, you can define a set of RESTful routes for a resource:
resources("posts");
This will create the following routes:
GET /posts
->src/actions/posts/indexAction.ts
GET /posts/new
->src/actions/posts/newAction.ts
POST /posts
->src/actions/posts/createAction.ts
GET /posts/:id
->src/actions/posts/showAction.ts
GET /posts/:id/edit
->src/actions/posts/editAction.ts
PUT /posts/:id
->src/actions/posts/updateAction.ts
PATCH /posts/:id
->src/actions/posts/updateAction.ts
DELETE /posts/:id
->src/actions/posts/destroyAction.ts
You can add middleware to all resource routes:
const postMiddlewares = [authenticate, logRequest];
resources("posts", postMiddlewares);
Resources can also be scoped:
scope("admin", [authenticate], () => {
resources("posts"); // Routes will be prefixed with /admin
resources("users"); // Routes will be prefixed with /admin
});
This creates routes like /admin/posts
, /admin/posts/:id
, etc.
When defining resources, you can customize which routes are created using only
or except
options:
// Create only index and show routes
resources("posts", { only: ["index", "show"] });
// Create all routes except destroy and edit
resources("posts", { except: ["destroy", "edit"] });
// Combine with middleware
resources("posts", [authenticate], { only: ["show", "update"] });
The available actions are:
index
- GET /postsnew
- GET /posts/newcreate
- POST /postsshow
- GET /posts/:idedit
- GET /posts/:id/editupdate
- PUT/PATCH /posts/:iddestroy
- DELETE /posts/:id
You can use either only
or except
, but not both at the same time. The only
option takes precedence if both are provided.
MIT.
Ilya N. Zykin | the-teacher