HTTP Method Decorators
HTTP method decorators are an alternative to convention-based routing for when you need custom paths, complex RESTful routes that don’t fit the convention pattern, non-CRUD actions, or multiple routes with the same HTTP verb on a single controller.
import { Controller, Get, Post, Put, Patch, Delete, All, RouterContext } from 'stratal/router'Example controller
Section titled “Example controller”import { Controller, Get, Post, Put, Patch, Delete, RouterContext } from 'stratal/router'import { z } from 'stratal/validation'
@Controller('/api/users', { tags: ['Users'] })export class UsersController { @Get('/', { response: z.object({ data: z.array(z.object({ id: z.string(), name: z.string() })), }), summary: 'List all users', }) listUsers(ctx: RouterContext) { return ctx.json({ data: [] }) }
@Get('/:id', { params: z.object({ id: z.string().uuid() }), response: z.object({ id: z.string(), name: z.string(), email: z.string() }), summary: 'Get user by ID', }) getUser(ctx: RouterContext) { return ctx.json({ id: ctx.param('id'), name: 'Alice', email: 'alice@example.com' }) }
@Post('/', { body: z.object({ name: z.string().min(1), email: z.string().email() }), response: z.object({ id: z.string(), name: z.string(), email: z.string() }), summary: 'Create a user', statusCode: 201, }) async createUser(ctx: RouterContext) { const body = await ctx.body<{ name: string; email: string }>() return ctx.json({ id: '1', ...body }, 201) }
@Put('/:id', { params: z.object({ id: z.string().uuid() }), body: z.object({ name: z.string(), email: z.string().email() }), response: z.object({ id: z.string(), name: z.string(), email: z.string() }), summary: 'Replace a user', }) async replaceUser(ctx: RouterContext) { const body = await ctx.body<{ name: string; email: string }>() return ctx.json({ id: ctx.param('id'), ...body }) }
@Patch('/:id', { params: z.object({ id: z.string().uuid() }), body: z.object({ name: z.string().optional(), email: z.string().email().optional() }), response: z.object({ id: z.string(), name: z.string(), email: z.string() }), summary: 'Update a user', }) async updateUser(ctx: RouterContext) { const body = await ctx.body<Partial<{ name: string; email: string }>>() return ctx.json({ id: ctx.param('id'), name: 'Alice', email: 'alice@example.com', ...body }) }
@Delete('/:id', { params: z.object({ id: z.string().uuid() }), response: z.object({ success: z.boolean() }), summary: 'Delete a user', }) deleteUser(ctx: RouterContext) { return ctx.json({ success: true }) }}Notice that method names are arbitrary — there is no IController interface to implement. The HTTP method and path are determined entirely by the decorator.
Decorator reference
Section titled “Decorator reference”Each decorator takes a path and an optional config object:
| Decorator | Signature | HTTP verb |
|---|---|---|
@Get | @Get(path, config?) | GET |
@Post | @Post(path, config?) | POST |
@Put | @Put(path, config?) | PUT |
@Patch | @Patch(path, config?) | PATCH |
@Delete | @Delete(path, config?) | DELETE |
@All | @All(path, config?) | All verbs |
Key points:
pathis relative to the controller’s base path.@Get('/:id')on a controller at/api/usersregistersGET /api/users/:id.- Method names are arbitrary. Name your methods whatever makes sense — there is no
IControllerinterface to implement. configaccepts the sameRouteConfigproperties as@Route(body, params, query, response, tags, security, summary, description, hideFromDocs) plusstatusCode. Thebodyproperty accepts aRouteBody— either a bare Zod schema (defaults toapplication/json) or a{ schema, contentType? }object for custom content types. Similarly,responseaccepts aRouteResponse— a bare Zod schema or a{ schema, description?, contentType? }object.
Complex RESTful paths
Section titled “Complex RESTful paths”HTTP method decorators shine when your routes don’t map to the six convention-based methods. Here are common patterns:
@Controller('/api/users', { tags: ['Users'] })export class UsersController { // Sub-resource route: upload a user's avatar @Post('/:id/avatar', { params: z.object({ id: z.string().uuid() }), response: z.object({ url: z.string().url() }), summary: 'Upload user avatar', statusCode: 201, }) async uploadAvatar(ctx: RouterContext) { // handle file upload return ctx.json({ url: 'https://cdn.example.com/avatars/1.png' }, 201) }
// Action route: non-CRUD operation @Post('/:id/activate', { params: z.object({ id: z.string().uuid() }), response: z.object({ activated: z.boolean() }), summary: 'Activate a user account', }) activateUser(ctx: RouterContext) { return ctx.json({ activated: true }) }
// Nested resource: deeply nested paths @Get('/:userId/posts/:postId', { params: z.object({ userId: z.string().uuid(), postId: z.string().uuid() }), response: z.object({ id: z.string(), title: z.string(), body: z.string() }), summary: 'Get a specific post by a user', }) getUserPost(ctx: RouterContext) { return ctx.json({ id: ctx.param('postId'), title: 'Hello World', body: 'Post content', }) }}The @All decorator
Section titled “The @All decorator”The @All decorator registers a handler for all HTTP methods on a given path. This is useful for catch-all routes like wildcard proxies or fallback handlers.
import { Controller, All, RouterContext } from 'stratal/router'
@Controller('/api/proxy')export class ProxyController { @All('/:path{.+}', { response: z.object({ proxied: z.boolean() }), summary: 'Proxy all requests', }) proxyAll(ctx: RouterContext) { return ctx.json({ proxied: true }) }}@All routes are automatically excluded from the OpenAPI specification. Since they match every HTTP method, including them would create ambiguous documentation. You do not need to set hideFromDocs: true.
Guards
Section titled “Guards”Guards work the same way with HTTP method decorators. Apply @UseGuards at the controller or method level:
import { Controller, Get, Delete, UseGuards, RouterContext } from 'stratal/router'import { AuthGuard } from './auth.guard'import { RolesGuard } from './roles.guard'
@Controller('/api/orders', { tags: ['Orders'] })@UseGuards(AuthGuard)export class OrdersController { @Get('/', { response: orderListSchema, summary: 'List orders', }) listOrders(ctx: RouterContext) { return ctx.json({ orders: [] }) }
@UseGuards(RolesGuard) @Delete('/:id', { params: z.object({ id: z.string() }), response: z.object({ success: z.boolean() }), summary: 'Delete an order', }) deleteOrder(ctx: RouterContext) { return ctx.json({ success: true }) }}Next steps
Section titled “Next steps”- Convention-based Routing for the
IControllerapproach with automatic method-to-verb mapping. - OpenAPI overview to see how decorator schemas generate API documentation.
- Guards guide for writing custom guards.