Skip to Content

Coming from NestJS

How NestJS concepts map onto Suku's.

Suku keeps what makes NestJS manageable: code organised by feature, routes next to their handlers, constructor injection, and declarative cross-cutting concerns. It uses fewer concepts to get there, and types flow from schemas instead of being written twice.

Concept map

NestJSSuku
ModuleFeature: feature('users', { ... })
ControllerA @Routes class, or plain routes
@Get(':id') on a methodfindOne = route.get('/:id', { ... }) as a field
@Param, @Query, @BodyThe params, query and body schemas
DTO class with class-validatorA Standard Schema (Zod, Valibot, ArkType)
@Injectable() provider@Service() class, or service(name, { ... }) without a class
Middleware, guard, interceptorMiddleware
PipeA schema
Exception filteronError
@SetMetadata and ReflectorThe route's meta, read as ctx.route.meta
ExecutionContextRequestContext
ConfigModule.forRoot()defineConfig(name, schema) and Env.load()
NestFactory.create(AppModule)Suku.create({ features: [...] })
Test.createTestingModule()Suku.create({ ..., configureServices }) and app.handle()

A controller, translated

// NestJS
@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.users.find(id);
  }

  @Post()
  @UseGuards(AuthGuard)
  create(@Body() dto: CreateUserDto) {
    return this.users.create(dto);
  }
}
// Suku
@Routes('/users')
export class UsersRoutes {
  constructor(private readonly users: UsersService) {}

  findOne = route.get('/:id', {
    params: z.object({ id: z.coerce.number().int() }),
    handle: ({ params }) => this.users.find(params.id),
  });

  create = route.post('/', {
    body: CreateUser,
    middlewares: [authenticate],
    handle: ({ body }) => this.users.create(body),
  });
}

What changes in practice

Lifetimes are strict. In NestJS a request-scoped provider quietly makes everything that depends on it request-scoped. In Suku a singleton that depends on a scoped service is rejected at startup. Take request-bound services through a route's services option.

One container. Every service is visible everywhere. There is no exports.

One cross-cutting concept. A guard is a middleware that throws. An interceptor is a middleware that changes the response. There is no RxJS.

Fetch, not Express. Handlers receive a Fetch Request and can return a Response.

Not available yet

Lifecycle hooks, WebSocket gateways, microservice transports, GraphQL and OpenAPI generation are not part of Suku today.

On this page