Skip to main content
Moving an HTTP API from NestJS to nestrs means trading TypeScript decorators for Rust proc macros, class-based providers for Arc-wrapped structs, and the Node.js runtime for Axum and Tower. The architectural ideas carry over almost directly—modules, controllers, guards, interceptors, pipes, and filters all have first-class analogues—but the implementation language is Rust, so some patterns look different even when they serve the same purpose.

Concept mapping at a glance

NestJSnestrsNotes
@Module(...)#[module(controllers = [...], providers = [...], imports = [...], exports = [...])]Applied to a plain struct
@Controller('prefix')#[controller(prefix = "/prefix", version = "v1")]Applied to a plain struct
@Injectable()#[injectable]Applied to a struct; resolved as Arc<T>
@Get(), @Post(), …#[get("path")], #[post("path")], …Inside a #[routes(state = S)] impl block
@Body(), @Param(), @Query()#[param::body], #[param::param], #[param::query]With #[use_pipes(ValidationPipe)] for validated DTOs
@UseGuards(G)#[use_guards(G)]G implements CanActivate
@UseInterceptors(I)#[use_interceptors(I)]I implements Interceptor
@UsePipes(ValidationPipe)#[use_pipes(ValidationPipe)]Validation is opt-in per route
@UseFilters(F)#[use_filters(F)]F implements ExceptionFilter
@SetMetadata('k', 'v')#[set_metadata("k", "v")]Stored in MetadataRegistry
@Roles('admin')#[roles("admin")]Shorthand metadata for role guards
@HttpCode(201)#[http_code(201)]Sets the response status code
@Header('k', 'v')#[response_header("k", "v")]Adds a response header
@Redirect(url)#[redirect("url")]Redirects the request
NestFactory.create(AppModule)NestFactory::create::<AppModule>()Returns a NestApplication builder
app.setGlobalPrefix('api').set_global_prefix("api")Builder method on NestApplication
app.enableVersioning(...).enable_uri_versioning("v") / .enable_header_versioning(...)Multiple versioning strategies
app.listen(3000).listen(3000).awaitAsync; also listen_graceful and listen_with_shutdown

Side-by-side code: modules and controllers

import { Module, Controller, Get, Param } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get(':id')
  findOne(@Param('id') id: string) {
    return { id };
  }
}

@Module({ controllers: [CatsController] })
export class AppModule {}

Side-by-side code: injectable providers

import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  findAll() { return []; }
}

@Module({ providers: [CatsService], exports: [CatsService] })
export class CatsModule {}

Side-by-side code: guards

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    return !!req.headers['authorization'];
  }
}

Side-by-side code: interceptors

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now();
    return next.handle().pipe(tap(() =>
      console.log(`Duration: ${Date.now() - start}ms`)
    ));
  }
}

Side-by-side code: DTO validation

import { IsEmail, IsString } from 'class-validator';

export class SignupDto {
  @IsEmail()
  email: string;

  @IsString()
  username: string;
}

Side-by-side code: metadata and roles

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Guards, interceptors, and filters — semantics

All three cross-cutting concerns work before or around the route handler, just like NestJS:
  • Guards (CanActivate) answer “may this request proceed?” and run before the handler. Returning Err(GuardError::Forbidden(...)) yields a 403 JSON response.
  • Interceptors (Interceptor) wrap the handler with next.run(req). Use them for logging, timing, and header injection.
  • Exception filters (ExceptionFilter) rewrite responses that carry an HttpException. Register globally with NestApplication::use_global_exception_filter or per-route with #[use_filters].
The pipeline order per route is fixed: global layers → guards → interceptors → filters → handler. See the middleware pipeline page for the complete contract.

DTOs and validation

NestJS uses class-validator and class-transformer for runtime validation. nestrs uses the #[dto] macro (which derives serde::Deserialize and validator::Validate) combined with ValidatedBody<T> or #[use_pipes(ValidationPipe)] on a handler.
Unknown JSON fields are rejected by default#[dto] emits #[serde(deny_unknown_fields)] automatically. Use #[dto(allow_unknown_fields)] to opt out when you intentionally accept forward-compatible clients.

Dependency injection differences

NestJS uses TypeScript constructor injection detected at runtime via reflect-metadata. nestrs constructs providers at compile time via the Injectable::construct method generated by #[injectable]. Each injectable receives a &ProviderRegistry and returns an Arc<Self>, pulling its own dependencies with registry.get::<DepType>(). There is no async constructor. Perform async I/O in on_module_init (called by the framework before listen starts serving traffic) or use ConfigurableModuleBuilder::for_root_async.

Configuration (ConfigModule → Rust patterns)

1

Parse environment variables at startup

Use std::env, dotenvy, or confy in main before calling NestFactory::create.
2

Pass options into a configurable module

Use ConfigurableModuleBuilder::for_root or for_root_async so injectables receive ModuleOptions<O, M> from the registry.
3

Use a typed settings injectable per bounded context

There is no global ConfigService token; create one #[injectable] settings struct per module if that suits your team’s style.

Testing

NestJS (Jest / Supertest)nestrs
TestingModuleBuild NestFactory::create::<M>().into_router() and use tower::ServiceExt::oneshot in tests
Isolated metadata / routesEnable the test-hooks feature and use registry clear helpers between tests
app.close()listen_with_shutdown drains in-flight requests on signal
// Integration test pattern
let router = NestFactory::create::<AppModule>().into_router();
let response = router
    .oneshot(
        axum::http::Request::builder()
            .uri("/cats/1")
            .body(axum::body::Body::empty())
            .unwrap(),
    )
    .await
    .unwrap();
assert_eq!(response.status(), 200);

Packages and dependencies

NestJS / npmRust / Cargo
package.json + lockfileCargo.toml + Cargo.lock (commit lock for binaries)
@nestjs/platform-expressThe nestrs crate (Axum is the only HTTP platform)
@nestjs/swaggernestrs-openapi crate + features = ["openapi"]
@nestjs/microservicesnestrs-microservices crate + transport feature flags
peerDependenciesCargo workspace [patch] / version alignment in root Cargo.toml

What is not yet in nestrs

The following NestJS features have no nestrs equivalent at this time:
  • Interactive TypeScript playground — nestrs is a compiled Rust crate; there is no in-browser REPL.
  • TypeScript-first API teaching tools — the CLI (nestrs-scaffold) handles code generation but does not produce TypeScript stubs.
  • reflect-metadata-style decorator introspection — all metadata in nestrs is compile-time; there is no runtime annotation system for arbitrary fields.
  • GraphQL subscriptions over WebSockets — nestrs-graphql and nestrs-ws ship separately; parity is partial.

NestFactory API

Bootstrap methods including microservice transports

NestApplication API

Full reference for every builder method

Routing macros

Controller, routes, and HTTP method macros

DI macros

Module, injectable, and metadata macros