From NestJS
Every NestJS construct next to its AponiaJS equivalent, side by side, including the ones that have no equivalent yet.
AponiaJS borrows structural vocabulary from NestJS, so most of a port is mechanical. This page puts the two versions of each construct next to each other. If you are still choosing rather than porting, read AponiaJS vs NestJS first.
What ports and what does not
| Nest concept | AponiaJS 0.6.0-alpha.18 |
|---|---|
| Modules | Implemented with imports, controllers, providers, and exports. |
| Singleton providers | Implemented. |
| Class/value/factory/alias providers | Implemented synchronously. |
| Decorated controllers | Implemented for all seven HTTP methods. |
| Request parameter decorators | Implemented, including named selection. |
| Validation | Implemented as route schemas and @Validation() model classes, not pipes. |
| Gateways | Implemented over native Elysia WebSockets, without Socket.IO semantics. |
| Exception filters | Not implemented; throw httpErrors.* for Problem Details responses. |
| Platform adapter | Elysia only. |
| Guards/interceptors/filters/middleware runtime | Not implemented. |
| Provider scopes and lifecycle hooks | Not implemented. |
| Testing module and overrides | Not implemented. |
| OpenAPI/auth/microservices | Not implemented. |
Packages
The decorator names survive the port. The imports do not.
NestJS
Core package plus a platform adapter.
import { Body, Controller, Get, Injectable, Module, Param, Post } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";AponiaJS
Decorators from common, the factory from the platform.
import { Body, Controller, Get, Injectable, Module, Param, Post } from "@aponiajs/common";
import { AponiaFactory } from "@aponiajs/platform-elysia";bun add @aponiajs/common@alpha @aponiajs/platform-elysia@alpha elysia@^1.4.29Bootstrap
NestJS
The factory chooses an HTTP adapter.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();AponiaJS
Top-level await; the platform is always Elysia.
const application = await AponiaFactory.create(AppModule);
await application.listen(3000);Modules
Identical, minus the features that have no runtime behind them yet.
NestJS
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}AponiaJS
Same four keys, validated at startup.
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}A provider a module never exported is a bootstrap error with a diagnostic rather than a runtime failure. See visibility.
Controllers
NestJS
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(":id")
findOne(@Param("id") id: string) {
return this.userService.findOne(id);
}
@Post()
create(@Body() body: CreateUserDto) {
return this.userService.create(body);
}
}AponiaJS
Same decorators; the body type is annotated, not inferred from a DTO class.
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(":id")
findOne(@Param("id") id: string) {
return this.userService.findOne(id);
}
@Post()
create(@Body() body: CreateUser) {
return this.userService.create(body);
}
}Two differences that bite during a port:
- there is no
@Injectable()-driven metadata for method decorators.@Injectable()exists only so TypeScript emits constructor parameter types; - handler parameter types come from your annotations, not from schema
inference, so keep the schema in a
constand derive withz.inferorStatic<typeof …>.
Providers
NestJS
Scope is a decorator option.
@Injectable({ scope: Scope.DEFAULT })
export class UserService {
findOne(id: string) {
return { id };
}
}AponiaJS
No scopes exist. Every provider is a startup singleton.
@Injectable()
export class UserService {
findOne(id: string) {
return { id };
}
}Non-class dependencies use an explicit token on both sides.
NestJS
@Module({
providers: [{ provide: "CONFIG", useValue: { url: "…" } }],
})
export class ConfigModule {}
constructor(@Inject("CONFIG") private readonly config: Config) {}AponiaJS
Same shape; see tokens and injection.
@Module({
providers: [{ provide: "CONFIG", useValue: { url: "…" } }],
})
export class ConfigModule {}
constructor(@Inject("CONFIG") private readonly config: Config) {}Async factory providers do not port — useFactory is synchronous. See
providers and
tokens.
Validation replaces pipes
This is the largest rewrite in a typical port: class-validator DTO classes
become schemas attached to the route.
NestJS
A DTO class, decorators, and a global ValidationPipe.
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
}
app.useGlobalPipes(new ValidationPipe());
@Post()
create(@Body() body: CreateUserDto) {
return body;
}AponiaJS
A schema on the decorator, rejected before the handler runs.
const createUser = {
body: z.object({ name: z.string().min(2) }),
};
@Post("/", createUser)
create(@Body() body: z.infer<(typeof createUser)["body"]>) {
return body;
}Any Standard Schema validator works — Zod,
ArkType, Valibot — as do TypeBox and Elysia's t. A rejected request answers
422. If you prefer the DTO-shaped arrangement, @Validation() model classes
keep the schema in a named class; see
validation.
Errors replace exception filters
NestJS
HttpException, optionally reshaped by a filter.
throw new NotFoundException("User not found");
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// reshape the response
}
}AponiaJS
RFC 9457 Problem Details, no filter layer.
throw httpErrors.notFound("User not found");
// {
// "type": "about:blank",
// "title": "Not Found",
// "status": 404,
// "detail": "User not found"
// }There is no exception filter runtime. The response shape is the framework's, documented in errors.
Gateways
NestJS
Socket.IO semantics by default.
@WebSocketGateway()
export class EventsGateway {
@SubscribeMessage("events")
handle(@MessageBody() data: string) {
return { event: "events", data };
}
}AponiaJS
Native Elysia and Bun sockets, same decorator names.
@WebSocketGateway()
export class EventsGateway {
@SubscribeMessage("events")
handle(@MessageBody() data: string) {
return { event: "events", data };
}
}The decorators match; the transport does not. There are no rooms, namespaces,
acknowledgements, or Socket.IO clients — messages are a plain
{ event, data } envelope over a native WebSocket. See
websockets.
Guards, interceptors, and middleware
These have no equivalent. Nothing ports; the behaviour is rewritten as an Elysia hook.
NestJS
Four extension points around the handler.
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
@Get()
findAll() {
return this.service.findAll();
}AponiaJS
An Elysia hook installed through native access.
const application = await AponiaFactory.create(AppModule, {
configureNative: (elysia) =>
elysia.onBeforeHandle(({ request, status }) => {
if (!request.headers.get("authorization")) return status(401);
}),
});The hook is not part of the module graph and cannot be applied per controller method. Details in native access and plugin modules.
Project layout
NestJS
src/
app.module.ts
main.ts
users/
users.controller.ts
users.service.ts
users.module.ts
dto/create-user.dto.tsAponiaJS
Same idea; DTO classes become model files.
src/
app.module.ts
main.ts
users/
users.controller.ts
users.service.ts
users.module.ts
users.model.tsDo the gap analysis before scheduling
Current limitations lists every runtime feature that is missing. Guards, interceptors, filters, middleware, provider scopes, lifecycle hooks, and the testing module are all on it.