Routing & HTTP Methods
Routing is how NestJS decides which controller method handles an incoming request. You attach metadata to controller classes and methods using decorators, and Nest builds a routing table at bootstrap that maps each HTTP verb and URL path to a handler. Getting routes right early matters: clean, predictable paths keep your API discoverable, make versioning painless, and avoid the subtle ordering bugs that creep in once wildcards enter the picture.
The controller prefix
A controller declares a base path with the @Controller() decorator. Every method route is appended to this prefix, so you only spell the resource name once.
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This returns all cats';
}
}
This handler answers GET /cats. The empty @Get() means “no additional path segment”, so it inherits the prefix exactly.
HTTP method decorators
Nest ships a decorator for every standard HTTP verb. Each one accepts an optional path string (or array of strings) appended to the controller prefix.
| Decorator | HTTP verb | Typical use |
|---|---|---|
@Get(path?) | GET | Read resources |
@Post(path?) | POST | Create resources |
@Put(path?) | PUT | Full replacement update |
@Patch(path?) | PATCH | Partial update |
@Delete(path?) | DELETE | Remove resources |
@Head(path?) | HEAD | Headers-only response |
@Options(path?) | OPTIONS | Preflight / capability checks |
@All(path?) | All verbs | Catch-all handler |
A full resource controller combines them:
import {
Controller,
Get,
Post,
Put,
Patch,
Delete,
Param,
Body,
} from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return [{ id: 1, name: 'Felix' }];
}
@Get(':id')
findOne(@Param('id') id: string) {
return { id: Number(id), name: 'Felix' };
}
@Post()
create(@Body() body: { name: string }) {
return { id: 2, ...body };
}
@Put(':id')
replace(@Param('id') id: string, @Body() body: { name: string }) {
return { id: Number(id), ...body };
}
@Patch(':id')
update(@Param('id') id: string, @Body() body: Partial<{ name: string }>) {
return { id: Number(id), ...body };
}
@Delete(':id')
remove(@Param('id') id: string) {
return { deleted: Number(id) };
}
}
Output:
$ curl http://localhost:3000/cats
[{"id":1,"name":"Felix"}]
$ curl -X POST http://localhost:3000/cats -H 'Content-Type: application/json' -d '{"name":"Mochi"}'
{"id":2,"name":"Mochi"}
Route path patterns and wildcards
Paths can include named parameters (:id) and pattern wildcards. In Nest 11 (which runs on Express 5 / a stricter path-matching engine by default), wildcard segments must be named: use *splat to capture the rest of a path rather than a bare *.
import { Controller, Get, Param } from '@nestjs/common';
@Controller('files')
export class FilesController {
// Matches /files/docs/guide/intro.md → splat = 'docs/guide/intro.md'
@Get('*splat')
serve(@Param('splat') splat: string) {
return { requestedPath: splat };
}
}
Heads up: under older Express 4 setups you may still see the legacy
@Get('ab*cd')style. On Nest 11’s default adapter, prefer named wildcards (*name) — bare asterisks and inline globs are no longer accepted and will throw a path-to-regexp error at startup.
Route ordering matters
Routes are matched in declaration order, top to bottom. A greedy wildcard or broad parameter route placed above a specific one will shadow it. Always declare the most specific routes first.
@Controller('users')
export class UsersController {
@Get('me') // specific — must come first
current() {
return { id: 'current-user' };
}
@Get(':id') // generic — anything else
findOne(@Param('id') id: string) {
return { id };
}
}
If @Get(':id') were declared first, a request to /users/me would be captured by it with id = 'me', never reaching the dedicated handler.
Sub-domain routing with host
Beyond paths, you can route on the request’s Host header. Pass a host option to @Controller() to bind a controller to a specific (sub)domain, and capture dynamic sub-domain segments with @HostParam().
import { Controller, Get, HostParam } from '@nestjs/common';
@Controller({ host: ':account.example.com' })
export class TenantController {
@Get()
info(@HostParam('account') account: string) {
return { tenant: account };
}
}
Output:
$ curl -H 'Host: acme.example.com' http://localhost:3000/
{"tenant":"acme"}
This is ideal for multi-tenant apps where each customer gets a branded sub-domain.
Versioning-friendly path design
Avoid baking version numbers into ad-hoc route strings. Instead enable Nest’s built-in versioning so the prefix is managed centrally and easy to evolve.
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' });
await app.listen(3000);
}
bootstrap();
@Controller({ path: 'cats', version: '2' })
export class CatsV2Controller {
@Get()
findAll() {
return { version: 2, cats: [] };
}
}
This serves the controller at /v2/cats while v1 controllers stay on /v1/cats, letting old and new clients coexist without touching every handler.
Best practices
- Declare specific routes before parameterized or wildcard routes so ordering never shadows a handler.
- Keep the controller prefix as the singular resource noun (
cats, notgetCats) and let HTTP verbs convey intent. - Use named wildcards (
*splat) for catch-all paths to stay compatible with Nest 11’s path matcher. - Reach for
app.enableVersioning()instead of hardcoding/v1/into route strings. - Reserve
host-based routing for genuine multi-tenant or sub-domain concerns; don’t overload it for normal pathing. - Keep handler methods thin — return data and push business logic into services injected via DI.