Routing
Routing is the mechanism that maps incoming requests to the correct controller methods. HonestJS uses a combination of decorators on your controller classes and methods to define the routes for your application.
Controllers
Controllers are responsible for handling incoming requests and returning responses. To create a controller, you use the @Controller() decorator on a class.
The @Controller() decorator can take an optional route prefix and configuration options. This prefix will be applied to all routes defined within that controller.
Example:
import { Controller, Get } from 'honestjs'
@Controller('users')
export class UsersController {
@Get()
findAll() {
return 'This route handles GET requests to /users'
}
@Get(':id')
findOne() {
return 'This route handles GET requests to /users/:id'
}
}In this example, the @Controller('users') decorator sets a base path for all routes in UsersController.
Controller Options
The @Controller() decorator accepts configuration options:
import { Controller, VERSION_NEUTRAL } from 'honestjs'
@Controller('users', {
prefix: 'api', // Override global prefix
version: 2 // Override global version
})
export class UsersController {
// Routes will be accessible at /api/v2/users
}
@Controller('health', {
version: VERSION_NEUTRAL // Accessible with and without version
})
export class HealthController {
// Routes will be accessible at both /health and /v1/health
}HTTP Method Decorators
To handle specific HTTP methods, HonestJS provides decorators for all standard methods:
@Get(path?: string, options?: HttpMethodOptions)@Post(path?: string, options?: HttpMethodOptions)@Put(path?: string, options?: HttpMethodOptions)@Delete(path?: string, options?: HttpMethodOptions)@Patch(path?: string, options?: HttpMethodOptions)@Options(path?: string, options?: HttpMethodOptions)@All(path?: string, options?: HttpMethodOptions)
These decorators are used on methods within a controller. They can take an optional path segment and options that will be appended to the controller's prefix.
import { Controller, Get, Post, Body, Param } from 'honestjs'
import UsersService from './users.service'
import type { CreateUserDto, User } from './users.types'
@Controller('users')
class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async createUser(@Body() body: CreateUserDto): Promise<User> {
return await this.usersService.create(body)
}
@Get()
async getUsers(): Promise<User[]> {
return await this.usersService.findAll()
}
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> {
return await this.usersService.findById(id)
}
@Put(':id')
async updateUser(@Param('id') id: string, @Body() body: Partial<CreateUserDto>): Promise<User> {
return await this.usersService.update(id, body)
}
@Delete(':id')
async deleteUser(@Param('id') id: string): Promise<void> {
await this.usersService.delete(id)
}
}Route Parameters
You can capture dynamic values from the URL path using route parameters. To define a route parameter, use a colon (:) in the path. To access its value, use the @Param() decorator in the method signature.
import { Controller, Get, Param } from 'honestjs'
@Controller('users')
class UsersController {
@Get(':id')
async getUser(@Param('id') id: string): Promise<User> {
const user = await this.usersService.findById(id)
if (!user) {
throw new Error('User not found')
}
return user
}
@Get(':userId/posts/:postId')
async getUserPost(@Param('userId') userId: string, @Param('postId') postId: string): Promise<Post> {
return await this.postsService.findByUserAndId(userId, postId)
}
}In this case, a GET request to /users/123 will call the getUser method, and the value of id will be "123".
Route Versioning
HonestJS supports flexible API versioning at multiple levels. You can set a global version, a version per controller, or even a version per route.
Global Versioning
You can set a global version prefix when creating your application.
const { app, hono } = await Application.create(AppModule, {
routing: {
version: 1
}
})With this configuration, all routes will be prefixed with /v1. For example, GET /users becomes GET /v1/users.
Controller-level Versioning
You can override the global version or set a specific version for a controller.
import { Controller, VERSION_NEUTRAL } from 'honestjs'
@Controller('users', { version: 2 })
export class UsersController {
// Routes in this controller will be prefixed with /v2
}
@Controller('health', { version: VERSION_NEUTRAL })
export class HealthController {
// This controller's routes will be accessible both with and without version
}
@Controller('legacy', { version: null })
export class LegacyController {
// This controller's routes will not have a version prefix, even if global version is set
}Route-level Versioning
You can also specify a version directly on a route decorator, which will override any controller or global settings.
import { Controller, Get, VERSION_NEUTRAL } from 'honestjs'
@Controller('users')
export class UsersController {
@Get('legacy', { version: 1 })
getLegacyUsers() {
// This will be accessible at /v1/users/legacy
}
@Get('new', { version: 2 })
getNewUsers() {
// This will be accessible at /v2/users/new
}
@Get('status', { version: VERSION_NEUTRAL })
getStatus() {
// This will be accessible at both /users/status and /v1/users/status
}
}Multiple Versions
You can make routes available at multiple versions simultaneously:
@Controller('users', { version: [1, 2] })
export class UsersController {
@Get()
getUsers() {
// This will be accessible at both /v1/users and /v2/users
}
}
@Controller('users')
export class UsersController {
@Get('compatible', { version: [1, 2, 3] })
getCompatibleUsers() {
// This will be accessible at /v1/users/compatible, /v2/users/compatible, and /v3/users/compatible
}
}Version-Neutral Routes
Sometimes you may want certain routes to be accessible both with and without version prefixes. For this, you can use the VERSION_NEUTRAL symbol.
import { Controller, Get, VERSION_NEUTRAL } from 'honestjs'
@Controller('health')
export class HealthController {
@Get('status', { version: VERSION_NEUTRAL })
getStatus() {
// This route will be accessible at both:
// - /health/status (without version)
// - /v1/health/status (with version, if global version is set)
return { status: 'ok' }
}
}Version-neutral routes are particularly useful for:
- Health check endpoints
- Status endpoints
- Public API endpoints that should remain accessible regardless of versioning
- Utility endpoints that don't change between API versions
Route Prefixes
Similar to versioning, you can control route prefixes at multiple levels:
// Global prefix
const { app, hono } = await Application.create(AppModule, {
routing: {
prefix: 'api'
}
})// Controller-level prefix
@Controller('users', { prefix: 'v2/api' })
export class UsersController {
// Routes will be accessible at /v2/api/users
}// Route-level prefix
@Controller('users')
export class UsersController {
@Get('data', { prefix: 'internal' })
getInternalData() {
// This will be accessible at /internal/users/data
}
}Route Information
You can get information about all registered routes in your application:
const { app, hono } = await Application.create(AppModule)
// Get all routes
const routes = app.getRoutes()
console.log(
'Registered routes:',
routes.map((r) => r.fullPath)
)
// Get routes by controller
const userRoutes = app.getRoutes().filter((r) => r.controller === 'UsersController')
// Get routes by method
const getRoutes = app.getRoutes().filter((r) => r.method === 'GET')Route Examples
Here are some comprehensive examples of route definitions:
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from 'honestjs'
@Controller('api/users', { version: 1 })
export class UsersController {
// GET /v1/api/users
@Get()
async findAll(@Query('page') page?: string, @Query('limit') limit?: string) {
return await this.usersService.findAll({
page: parseInt(page || '1'),
limit: parseInt(limit || '10')
})
}
// GET /v1/api/users/:id
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.usersService.findById(id)
}
// POST /v1/api/users
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return await this.usersService.create(createUserDto)
}
// PUT /v1/api/users/:id
@Put(':id')
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return await this.usersService.update(id, updateUserDto)
}
// DELETE /v1/api/users/:id
@Delete(':id')
async remove(@Param('id') id: string) {
return await this.usersService.delete(id)
}
// GET /v1/api/users/:id/posts
@Get(':id/posts')
async getUserPosts(@Param('id') userId: string) {
return await this.postsService.findByUserId(userId)
}
}
@Controller('health', { version: VERSION_NEUTRAL })
export class HealthController {
// GET /health/status AND GET /v1/health/status
@Get('status')
async getStatus() {
return { status: 'ok', timestamp: new Date().toISOString() }
}
}By combining these features, you can build well-structured and maintainable routing for your applications with flexible versioning and prefixing strategies.
Having routing issues? See the FAQ for common questions, or the Troubleshooting page for edge cases like
VERSION_NEUTRALbehavior and duplicate route detection.
