Skip to content
NestJS ns fundamentals 4 min read

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.

DecoratorHTTP verbTypical use
@Get(path?)GETRead resources
@Post(path?)POSTCreate resources
@Put(path?)PUTFull replacement update
@Patch(path?)PATCHPartial update
@Delete(path?)DELETERemove resources
@Head(path?)HEADHeaders-only response
@Options(path?)OPTIONSPreflight / capability checks
@All(path?)All verbsCatch-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, not getCats) 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.
Last updated June 14, 2026
Was this helpful?