Route Files

Dinner reads OpenAPI YAML files and turns each route operation into an Express handler. Start with one route file per feature area:

server/openapi/
├── users.yaml
├── posts.yaml
├── auth.yaml
└── shared/schemas.yaml

A Route Is a Path + Method

The smallest useful Dinner route has a path, an HTTP method, a controller, and an action. When a request matches the path and method, Dinner calls the configured action.

server/openapi/users.yaml
# server/openapi/users.yaml
paths:
  /api/users:
    get:
      summary: List users
      x-controller: user.controller
      x-action: list

Add Path Params

Use {name} in the path when part of the URL should be captured. The captured value is exposed by name on req.params.

server/openapi/users.yaml
# server/openapi/users.yaml
paths:
  /api/users/{id:\d+}:
    get:
      summary: Get one user
      x-controller: user.controller
      x-action: getById

Path Token Names Become Param Names

Parameter names come from the path tokens. A request to /api/teams/core/users/42 matches /api/teams/{teamSlug}/users/{userId} and produces { teamSlug: "core", userId: "42" }.

server/openapi/users.yaml
# server/openapi/users.yaml
paths:
  /api/teams/{teamSlug}/users/{userId}:
    get:
      summary: Get a team member
      x-controller: team-user.controller
      x-action: getMember

Restrict Matches with Regex

Add a regex after a colon when a route should only match specific values. Dinner uses the regex while matching the route, then exposes the extracted values by name on req.params.

Dinner uses path-to-regex for route matching. Prefer OpenAPI-style braces in YAML, and use regex inside the token when the URL must be constrained. Query strings are ignored before matching, and the path match is case-sensitive.

Route patternIncoming URLAcceptedParams
/users/usersYes{}
/users/UsersNoCase-sensitive path
/users/{id}/users/aliceYes{ id: "alice" }
/users/{id}/users/alice/profileNoSingle param stops at /
/users/{id:\d+}/users/42Yes{ id: "42" }
/users/{id:\d+}/users/aliceNoNo route match
/teams/{teamId}/users/{userId}/teams/core/users/42Yes{ teamId: "core", userId: "42" }
/users/{slug:[a-z-]+}/users/ada-lovelaceYes{ slug: "ada-lovelace" }
/users/{slug:[a-z-]+}/users/AdaNoNo route match
/posts/{slug?}/postsYes{}
/posts/{slug?}/posts/introYes{ slug: "intro" }
/files/{path:.*}/files/a/b/c.txtYes{ path: "a/b/c.txt" }
/files/{path:.+}/filesNoRequires a path value
/assets/file-{name:[a-z]+}.png/assets/file-logo.pngYes{ name: "logo" }
/assets/{name}.{ext:png|jpg}/assets/logo.gifNoExtension rejected
/users/:id(\d+)/users/42Yes{ id: "42" }

Do Not Add Path Parameter Blocks

Path params come from route tokens only. Do not add OpenAPI parameters blocks for values already declared in the path. Use route regex when the path itself should reject a value.

server/openapi/users.yaml
# server/openapi/users.yaml
paths:
  /api/users:
    get:
      summary: List users
      x-controller: user.controller
      x-action: list

Organizing Routes with Modules

Why Direct Routes Get Repetitive

Direct routes are useful for learning and small files. As a feature grows, the same URL prefix and shared route settings start repeating.

server/openapi/users.yaml
# server/openapi/users.yaml
paths:
  /api/users:
    get:
      x-controller: user.controller
      x-action: list

  /api/users/{id:\d+}:
    get:
      x-controller: user.controller
      x-action: getById

  /api/users/{id:\d+}/settings:
    get:
      x-controller: user-settings.controller
      x-action: get

Introduce a Module

A module groups related routes under a shared basePath. Dinner combines the module base path with each nested path, so /api/users plus /{id:\d+} becomes /api/users/{id:\d+}.

server/openapi/users.yaml
# server/openapi/users.yaml
module:
  users:
    basePath: /api/users
    paths:
      /:
        get:
          summary: List users
          x-controller: user.controller
          x-action: list

      /{id:\d+}:
        get:
          summary: Get one user
          x-controller: user.controller
          x-action: getById

Add Module Middleware

Module middleware applies to every route inside the module. Route-level middleware can still be added later when one operation needs extra handling.

server/openapi/users.yaml
# server/openapi/users.yaml
module:
  users:
    basePath: /api/users
    x-middleware:
      - auth
    paths:
      /:
        get:
          x-controller: user.controller
          x-action: list

      /{id:\d+}:
        get:
          x-controller: user.controller
          x-action: getById

Complete Users Module

server/openapi/users.yaml
# server/openapi/users.yaml
module:
  users:
    basePath: /api/users
    x-middleware:
      - auth
    paths:
      /:
        get:
          summary: List users
          x-controller: user.controller
          x-action: list
          responses:
            '200':
              description: List of users

        post:
          summary: Create user
          x-controller: user.controller
          x-action: create
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/CreateUserRequest'
          responses:
            '201':
              description: Created user

      /{id:\d+}:
        get:
          summary: Get one user
          x-controller: user.controller
          x-action: getById
          responses:
            '200':
              description: User details

        put:
          summary: Update user
          x-controller: user.controller
          x-action: update
          x-middleware:
            - validate_user
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/UpdateUserRequest'
          responses:
            '200':
              description: Updated user

Standalone Dinner Example

Controller Argument Wiring

In a normal @noego/app project, App owns this wiring and provides the established controller argument pattern. You usually do not configure controller_args_provider yourself.

Dinner still exposes the lower-level hook so it can run in isolation. When using @noego/dinner without @noego/app, define the action argument shape with controller_args_provider. Include params: req.params only if standalone controllers should read path params directly.

server.ts
// server.ts
import express from "express";
import { Server } from "@noego/dinner";

const app = express();
app.use(express.json());

// Standalone Dinner only. @noego/app already wires this pattern for app projects.
await Server.createServer({
  openapi_path: "server/openapi/users.yaml",
  controllers_base_path: "server/controllers",
  server: app,
  controller_builder: async (Controller) => new Controller(),
  controller_args_provider: async (req, res, context) => ({
    req,
    res,
    context,
    body: req.body,
    params: req.params,
    query: req.query,
  }),
});

Standalone Controller Example

The controller reads the names declared in the route tokens. Query params remain separate from path params and come from req.query or whatever query field the standalone provider exposes.

server/controllers/user.controller.ts
// server/controllers/user.controller.ts
type UserRouteParams = {
  id: string;
};

type UserRouteArgs = {
  params: UserRouteParams;
  query: {
    include?: string;
  };
};

export default class UserController {
  async getById({ params, query }: UserRouteArgs) {
    const id = Number(params.id);
    const include = query.include;

    return {
      id,
      include,
    };
  }
}

Path params are always strings when they reach your controller. If the route regex only allows digits, still convert the value in your controller or service before using it as a number.

NoEgo

© 2025 NoEgo. All rights reserved.