Skip to main content

Controllers & Endpoints

Controllers handle incoming HTTP requests. Endpoints define the routes and schemas for each operation.

What are Controllers?

Controllers are classes that organize related HTTP handlers. They're decorated with @Controller() and contain endpoint methods.

Key characteristics:

  • Group related endpoints together (e.g., all user operations)
  • Use dependency injection to access services
  • Handle HTTP concerns (status codes, headers)
  • Delegate business logic to services

Defining Controllers

import { Controller, Endpoint, EndpointParams, HttpCode } from '@navios/core'
import { inject } from '@navios/di'

@Controller()
class UserController {
private userService = inject(UserService)

@Endpoint(getUser)
async getUser(params: EndpointParams<typeof getUser>) {
return this.userService.findById(params.urlParams.userId)
}

@Endpoint(createUser)
@HttpCode(201)
async createUser(params: EndpointParams<typeof createUser>) {
return this.userService.create(params.data)
}
}

Defining Endpoints

Endpoints are defined using @navios/builder and attached to methods with @Endpoint():

import { builder } from '@navios/builder'

import { z } from 'zod'

const API = builder()

export const getUser = API.declareEndpoint({
method: 'GET',
url: '/users/$userId',
responseSchema: z.object({
id: z.string(),
name: z.string(),
}),
})

export const createUser = API.declareEndpoint({
method: 'POST',
url: '/users',
requestSchema: z.object({
name: z.string(),
email: z.string().email(),
}),
responseSchema: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
})

For complete endpoint definition syntax including URL parameters, query parameters, headers, and advanced options, see the Builder documentation.

EndpointParams

The EndpointParams type provides typed access to all request data:

interface EndpointParams<T> {
urlParams: { ... } // URL path parameters (e.g., /users/$userId)
query: { ... } // Query string parameters
data: { ... } // Request body
}

TypeScript infers the shape from your endpoint definition:

@Endpoint(updateUser)
async updateUser(params: EndpointParams<typeof updateUser>) {
const { userId } = params.urlParams // From URL
const { name, email } = params.data // From body
const { page } = params.query // From query string
}

Response Status Codes

Set custom status codes with @HttpCode():

import { HttpCode } from '@navios/core'

@Controller()
class UserController {
@Endpoint(createUser)
@HttpCode(201)
async createUser(params: EndpointParams<typeof createUser>) {
return this.userService.create(params.data)
}

@Endpoint(deleteUser)
@HttpCode(204)
async deleteUser(params: EndpointParams<typeof deleteUser>) {
await this.userService.delete(params.urlParams.userId)
}
}

Response Headers

Set custom headers with @Header():

import { Header } from '@navios/core'

@Controller()
class ApiController {
@Endpoint(getData)
@Header('Cache-Control', 'max-age=3600')
@Header('X-Custom-Header', 'value')
async getData() {
return { data: 'cached' }
}
}

Dependency Injection

Controllers typically inject services to handle business logic:

import { Logger } from '@navios/core'
import { inject } from '@navios/di'

@Controller()
class UserController {
private userService = inject(UserService)
private logger = inject(Logger, { context: 'UserController' })

@Endpoint(getUser)
async getUser(params: EndpointParams<typeof getUser>) {
this.logger.log(`Fetching user ${params.urlParams.userId}`)
return this.userService.findById(params.urlParams.userId)
}
}

For more on dependency injection, see the Services guide.

Best Practices

Keep controllers thin: Delegate business logic to services. Controllers handle HTTP concerns only.

// Good - controller delegates to service
@Endpoint(getUser)
async getUser(params: EndpointParams<typeof getUser>) {
return this.userService.findById(params.urlParams.userId)
}

// Avoid - business logic in controller
@Endpoint(getUser)
async getUser(params: EndpointParams<typeof getUser>) {
const user = await this.db.users.findUnique({ where: { id: params.urlParams.userId } })
if (!user) throw new NotFoundException()
return user
}

Group related endpoints: All user operations in UserController, all order operations in OrderController.

Use type-safe endpoints: Always use Builder for endpoint definitions. It provides compile-time safety and runtime validation.