Skip to content

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.

  1. 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 } });
      }
    }
    JustScale
    export class UserService extends defineService({
      inject: { users: UserRepository },
      factory: ({ users }) => ({
        findByEmail: (email: string) =>
          users.findOne(User.fields.email.eq(email)),
      }),
    }) {}
  2. 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);
      }
    }
    JustScale
    import { 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),
      }),
    });
  3. 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 {}
    JustScale
    import JustScale from '@justscale/core';
    
    export const app = JustScale()
      .add(UserService)
      .add(UsersController);
  4. 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.

    NestJS
    export class CreateUserDto {
      @IsEmail() email: string;
    }
    
    @Post()
    create(@Body() dto: CreateUserDto) { /* ... */ }
    JustScale
    import { Post } from '@justscale/http';
    import { z } from 'zod';
    
    create: Post('/users')
      .body(z.object({ email: z.string().email() }))
      .handle(({ body }) => { /* body is typed + validated */ }),
  5. 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);
      }
    }
    JustScale
    import { 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.

Learn more