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 valueResolves to
auth.cookieserver/middleware/auth/cookie.tsdefault export
auth.admin:is_adminserver/middleware/auth/admin.tsnamed export is_admin
auth.admin:is_admin,verify_scopeSame file — two named exports, executed in order
Resolution examples
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
# server/openapi/admin/admin.yaml
paths:
  /admin:
    get:
      x-controller: admin.controller
      x-action: listAdmins
      x-middleware:
        - auth.admin:is_admin

Single 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 shorthand
# Single middleware (string shorthand)
x-middleware: auth.cookie

# Equivalent array form
x-middleware:
  - auth.cookie
Multiple middleware
# Multiple middleware — executed in order
x-middleware:
  - auth.cookie
  - auth.validate_token
  - rate_limit:api

Writing 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
// 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
// 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
// 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
// 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
// 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:

server:
  middleware: server/middleware
  watch:
    - server/middleware/**/*.ts

Project Structure

server/
  ├── 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
NoEgo

© 2025 NoEgo. All rights reserved.