Controllers

Handle incoming HTTP requests and define your API routes.

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:

DecoratorHTTP 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.

DecoratorValidatesSource
@ValidateBody(schema)Request bodyawait ctx.json<T>() (validated after decorator runs)
@ValidateQuery(schema)Query parametersctx.query
@ValidateParams(schema)Path parametersctx.params