Controllers
Controllers are responsible for handling incoming HTTP requests and returning responses. A controller groups related routes under a common path prefix.
Defining a controller
Use the @Controller() decorator to define a controller and its path prefix:
import { Controller, Get, Post, type RequestContext } from '@miiajs/core'
@Controller('/users')
class UserController {
@Get('/')
list() {
return [{ id: 1, name: 'Alice' }]
}
@Get('/:id')
findOne(ctx: RequestContext) {
return { id: ctx.params.id }
}
@Post('/')
async create(ctx: RequestContext) {
return await ctx.json()
}
}
Controllers must be registered in a Module to be discovered by the framework.
Route decorators
MiiaJS provides decorators for all standard HTTP methods:
| Decorator | HTTP Method |
|---|---|
@Get(path?) | GET |
@Post(path?) | POST |
@Put(path?) | PUT |
@Patch(path?) | PATCH |
@Delete(path?) | DELETE |
@Head(path?) | HEAD |
@Options(path?) | OPTIONS |
The path argument is optional and defaults to '' (the controller prefix itself).
RequestContext
Every route handler receives a RequestContext object as its first argument:
interface RequestContext {
req: Request // Native Fetch API Request
res: ResponseBuilder // Fluent response builder
params: Record<string, string> // URL path parameters
query: Record<string, string> // Parsed query string
rawQuery: URLSearchParams // Raw query for multi-value params
json<T = any>(): Promise<T> // Parsed JSON body (cached)
text(): Promise<string> // Raw body as text (cached)
}
query and rawQuery are lazy-loaded on first access for better performance.
RequestContext is extensible via declaration merging. For example, @miiajs/auth adds the user property automatically when installed.
Route parameters
Dynamic segments in the path are extracted as ctx.params:
@Get('/:userId/posts/:postId')
getPost(ctx: RequestContext) {
const { userId, postId } = ctx.params
return { userId, postId }
}
Response handling
Route handlers can return values in several ways:
Auto JSON
Return any object or array and it will be serialized as JSON with status 200:
@Get('/')
list() {
return [{ id: 1 }, { id: 2 }]
}
Custom status
Use the @Status() decorator to set a different status code:
import { Status } from '@miiajs/core'
@Post('/')
@Status(201)
create(ctx: RequestContext) {
return { id: 3, created: true }
}
Native Response
Return a Response object for full control:
@Get('/download')
download() {
return new Response('raw body', {
status: 200,
headers: { 'Content-Type': 'text/plain' },
})
}
Response builder
Use ctx.res for a fluent API:
@Get('/html')
page(ctx: RequestContext) {
ctx.res
.status(200)
.header('X-Request-Id', 'abc')
.html('<h1>Hello</h1>')
}
See Response for more details.
Validation
MiiaJS provides built-in validation decorators that work with any ZodLike schema (Zod, or any object with safeParse()):
import { ValidateBody, ValidateQuery, ValidateParams } from '@miiajs/core'
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const QuerySchema = z.object({
page: z.string().transform(Number).default('1'),
limit: z.string().transform(Number).default('10'),
})
type CreateUserInput = z.infer<typeof CreateUserSchema>
@Controller('/users')
class UserController {
@Post('/')
@Status(201)
@ValidateBody(CreateUserSchema)
async create(ctx: RequestContext) {
// After @ValidateBody runs, ctx.json<T>() returns the validated (and possibly transformed) data.
const data = await ctx.json<CreateUserInput>()
return data
}
@Get('/')
@ValidateQuery(QuerySchema)
list(ctx: RequestContext) {
return { page: ctx.query.page, limit: ctx.query.limit }
}
}
Handler methods that read the request body must be async, since ctx.json() returns a Promise.
Validation decorators throw UnprocessableException (422) with detailed error messages on failure.
| Decorator | Validates | Source |
|---|---|---|
@ValidateBody(schema) | Request body | await ctx.json<T>() (validated after decorator runs) |
@ValidateQuery(schema) | Query parameters | ctx.query |
@ValidateParams(schema) | Path parameters | ctx.params |