Overview
In @noego/dinner,
middleware lets you intercept requests before they reach your controller. Middleware functions follow the standard Express signature (req, res, next) and are wired
declaratively through your OpenAPI specification using the x-middleware extension.
Dinner resolves each middleware entry against the directory configured as server.middleware in noego.config.yml, then injects
the resulting functions into the Express pipeline in the order they are declared.
Path Resolution
Each middleware entry in x-middleware is a dot-separated path that maps to a file under your middleware directory. Colons separate
the file path from named exports.
Resolution Rules
| x-middleware value | Resolves to |
|---|---|
| auth.cookie | server/middleware/auth/cookie.ts — default export |
| auth.admin:is_admin | server/middleware/auth/admin.ts — named export is_admin |
| auth.admin:is_admin,verify_scope | Same file — two named exports, executed in order |
auth.cookie → server/middleware/auth/cookie.ts (default export)
auth.admin → server/middleware/auth/admin.ts (default export)
auth.admin:is_admin → server/middleware/auth/admin.ts (named export "is_admin")
auth.admin:is_admin,verify_scope → server/middleware/auth/admin.ts (two named exports, run in order)
rate_limit:api → server/middleware/rate_limit.ts (named export "api")Declaring Middleware
Attaching to Routes
Attach middleware to individual operations in your OpenAPI spec. Middleware executes in list order, before the controller action is invoked.
# server/openapi/admin/admin.yaml
paths:
/admin:
get:
x-controller: admin.controller
x-action: listAdmins
x-middleware:
- auth.admin:is_adminSingle vs. Multiple
A single middleware can be written as a plain string or as a one-element array. Use the array form when you need more than one:
# Single middleware (string shorthand)
x-middleware: auth.cookie
# Equivalent array form
x-middleware:
- auth.cookie# Multiple middleware — executed in order
x-middleware:
- auth.cookie
- auth.validate_token
- rate_limit:apiWriting Middleware
Default Export (Simple)
Most middleware is a single function exported as default.
You can wrap any existing Express-compatible middleware library:
// server/middleware/auth/cookie.ts
import cookieParser from 'cookie-parser';
const secret = process.env.COOKIE_SECRET;
export default cookieParser(secret);Named Exports
When a single file contains multiple related middleware functions, export them by name
and reference each one with the : separator:
// server/middleware/auth/admin.ts
import type { Request, Response, NextFunction } from "express";
export function is_admin(req: Request, res: Response, next: NextFunction) {
if (!req.user?.roles?.includes("admin")) {
return res.status(403).json({ error: "Admin role required" });
}
next();
}
export function verify_scope(req: Request, res: Response, next: NextFunction) {
const required = (req.headers["x-required-scope"] as string) || "";
if (!req.user?.scopes?.includes(required)) {
return res.status(403).json({ error: `Scope "${required}" required` });
}
next();
}Thin Wrapper + Logic Pattern
For middleware that requires dependency injection (services, repositories), use a thin wrapper that delegates to a dedicated Logic class. The wrapper lives in the middleware directory and uses the IoC container to resolve the Logic class. This keeps the Logic fully testable in isolation.
1. The Logic Class
Pure business logic with injected dependencies — no Express types needed:
// server/logic/auth-middleware.logic.ts
import { Component, Inject } from "@noego/ioc";
import { AuthService } from "../service/auth";
@Component()
export default class AuthMiddlewareLogic {
constructor(@Inject(AuthService) private authService: AuthService) {}
async validateToken(token: string) {
const payload = this.authService.verifyJwt(token);
const user = await this.authService.findUserById(payload.sub);
if (!user) throw { status: 401, message: "User not found" };
if (user.deleted_at) throw { status: 401, message: "Account suspended" };
return { id: user.id, role: user.role, scopes: user.scopes };
}
}2. The Thin Wrapper
Resolves the Logic class via the container and bridges it to the Express middleware signature:
// server/middleware/auth/validate-token.ts
import { getContainer } from "@noego/ioc";
import AuthMiddlewareLogic from "../../logic/auth-middleware.logic";
export default async function validateToken(req: any, res: any, next: any) {
try {
const token =
req.headers.authorization?.replace("Bearer ", "") ||
req.cookies?.auth_token ||
req.headers["x-auth-token"];
if (!token) {
res.status(401).json({ error: "Missing authentication token" });
return;
}
const container = getContainer();
const authLogic = container.get(AuthMiddlewareLogic);
const actor = await authLogic.validateToken(token);
req.user = actor;
next();
} catch (error: any) {
res.status(error.status || 500).json({ error: error.message });
}
}Testing Middleware
Test the Logic class in isolation with mocked dependencies. This gives you fast unit tests without spinning up an Express server:
// test/unit/auth-middleware.logic.test.ts
describe('AuthMiddlewareLogic', () => {
let logic: AuthMiddlewareLogic;
let mockAuthService: jest.Mocked<AuthService>;
beforeEach(() => {
mockAuthService = createMockService();
logic = new AuthMiddlewareLogic(mockAuthService);
});
it('should return actor for valid token', async () => {
const token = jwt.sign({ id: 123 }, process.env.JWT_SECRET);
mockAuthService.findUserById.mockResolvedValue({
id: 123, role: 'user', scopes: ['read'], deleted_at: null,
});
const actor = await logic.validateToken(token);
expect(actor).toEqual({ id: 123, role: 'user', scopes: ['read'] });
expect(mockAuthService.findUserById).toHaveBeenCalledWith(123);
});
it('should throw for suspended user', async () => {
const token = jwt.sign({ id: 123 }, process.env.JWT_SECRET);
mockAuthService.findUserById.mockResolvedValue({
id: 123, deleted_at: '2025-01-01',
});
await expect(logic.validateToken(token))
.rejects.toMatchObject({ status: 401 });
});
});Configuration Reference
forge.config.yaml
The middleware directory is specified under server.middleware.
All .ts files inside are watched for changes during development:
middleware: server/middleware
watch:
- server/middleware/**/*.ts
Project Structure
├── middleware/
│ └── auth/
│ ├── cookie.ts # default export
│ ├── admin.ts # named exports: is_admin, verify_scope
│ └── validate-token.ts # thin wrapper → Logic class
├── logic/
│ └── auth-middleware.logic.ts
└── openapi/
└── admin/admin.yaml # x-middleware references