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
| NestJS | Suku |
|---|---|
| Module | Feature: feature('users', { ... }) |
| Controller | A @Routes class, or plain routes |
@Get(':id') on a method | findOne = route.get('/:id', { ... }) as a field |
@Param, @Query, @Body | The params, query and body schemas |
DTO class with class-validator | A Standard Schema (Zod, Valibot, ArkType) |
@Injectable() provider | @Service() class, or service(name, { ... }) without a class |
| Middleware, guard, interceptor | Middleware |
| Pipe | A schema |
| Exception filter | onError |
@SetMetadata and Reflector | The route's meta, read as ctx.route.meta |
ExecutionContext | RequestContext |
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.