Migration guide
Migrating from NestJS to JustScale
Migrating from NestJS to JustScale is mostly a one-to-one translation: NestJS providers become JustScale services, controllers stay controllers, and modules become explicit app composition. The biggest change is conceptual rather than mechanical — decorators and reflect-metadata are replaced by function-based dependency injection, and background jobs you ran on a queue typically become durable processes.
This guide walks through each piece in the order you will hit it: services, controllers and routes, app wiring, validation, guards, and background work.
Providers become services
A NestJS @Injectable() provider becomes a defineService with an inject map and a factory. Constructor injection is replaced by the injected dependencies passed to the factory.
NestJS@Injectable() export class UserService { constructor(private readonly users: UserRepository) {} findByEmail(email: string) { return this.users.findOne({ where: { email } }); } }JustScaleexport class UserService extends defineService({ inject: { users: UserRepository }, factory: ({ users }) => ({ findByEmail: (email: string) => users.findOne(User.fields.email.eq(email)), }), }) {}Controllers and routes
Controllers stay controllers, but routes are defined with route-builder factories instead of method decorators. Path params can be turned into typed model references with .types().
NestJS@Controller('users') export class UsersController { constructor(private readonly svc: UserService) {} @Get(':id') get(@Param('id') id: string) { return this.svc.findById(id); } }JustScaleimport { createController } from '@justscale/core'; import { Get } from '@justscale/http'; export const UsersController = createController({ inject: { svc: UserService }, routes: () => ({ get: Get('/users/:user').types({ user: User }).handle(({ user }) => user), }), });Modules become app composition
Instead of @Module metadata, you compose the app explicitly. Add services and controllers to a JustScale() app; there is no module graph to maintain.
NestJS@Module({ providers: [UserService], controllers: [UsersController], }) export class AppModule {}JustScaleimport JustScale from '@justscale/core'; export const app = JustScale() .add(UserService) .add(UsersController);DTOs and pipes become schema validation
Validation pipes and class-validator DTOs become a schema on the route. The handler receives a typed, validated body.
NestJSexport class CreateUserDto { @IsEmail() email: string; } @Post() create(@Body() dto: CreateUserDto) { /* ... */ }JustScaleimport { Post } from '@justscale/http'; import { z } from 'zod'; create: Post('/users') .body(z.object({ email: z.string().email() })) .handle(({ body }) => { /* body is typed + validated */ }),Background jobs become durable processes
Queue-based background work (for example a BullMQ processor) usually becomes a durable process: plain async code that suspends on signals or timeouts and resumes after a restart, with no separate worker or queue.
NestJS + BullMQ@Processor('orders') export class OrderProcessor { @Process() async handle(job: Job) { await chargeCard(job.data.orderId); } }JustScaleimport { createProcess } from '@justscale/core/process'; export const orderFulfillment = createProcess({ path: '/order/:order/fulfillment', inject: { payments: PaymentService }, async handler({ payments }, { order }) { await payments.charge(order); // survives restarts }, });
Frequently asked questions
How long does migrating from NestJS take?
Most of the work is mechanical: providers to services and controllers to route-builder controllers. The conceptual shift is dropping decorators for function-based DI and moving queue jobs to durable processes.
Do I have to rewrite my NestJS guards?
No rewrite of the logic is needed; NestJS guards map to JustScale guards and interceptors map to middleware.
Can I keep BullMQ during migration?
Yes. You can migrate incrementally and convert queue jobs to durable processes one at a time.