Route Files
Dinner reads OpenAPI YAML files and turns each route operation into an Express handler. Start with one route file per feature area:
├── 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
paths:
/api/users:
get:
summary: List users
x-controller: user.controller
x-action: listAdd 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
paths:
/api/users/{id:\d+}:
get:
summary: Get one user
x-controller: user.controller
x-action: getByIdPath 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
paths:
/api/teams/{teamSlug}/users/{userId}:
get:
summary: Get a team member
x-controller: team-user.controller
x-action: getMemberRestrict 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 pattern | Incoming URL | Accepted | Params |
|---|---|---|---|
| /users | /users | Yes | {} |
| /users | /Users | No | Case-sensitive path |
| /users/{id} | /users/alice | Yes | { id: "alice" } |
| /users/{id} | /users/alice/profile | No | Single param stops at / |
| /users/{id:\d+} | /users/42 | Yes | { id: "42" } |
| /users/{id:\d+} | /users/alice | No | No route match |
| /teams/{teamId}/users/{userId} | /teams/core/users/42 | Yes | { teamId: "core", userId: "42" } |
| /users/{slug:[a-z-]+} | /users/ada-lovelace | Yes | { slug: "ada-lovelace" } |
| /users/{slug:[a-z-]+} | /users/Ada | No | No route match |
| /posts/{slug?} | /posts | Yes | {} |
| /posts/{slug?} | /posts/intro | Yes | { slug: "intro" } |
| /files/{path:.*} | /files/a/b/c.txt | Yes | { path: "a/b/c.txt" } |
| /files/{path:.+} | /files | No | Requires a path value |
| /assets/file-{name:[a-z]+}.png | /assets/file-logo.png | Yes | { name: "logo" } |
| /assets/{name}.{ext:png|jpg} | /assets/logo.gif | No | Extension rejected |
| /users/:id(\d+) | /users/42 | Yes | { 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
paths:
/api/users:
get:
summary: List users
x-controller: user.controller
x-action: listOrganizing 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
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: getIntroduce 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
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: getByIdAdd 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
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: getByIdComplete Users Module
# 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 userStandalone 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
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
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.