Skip to main content

Quick Start with NestJS

This guide walks you through building a minimal OPRA HTTP API using NestJS. Controllers are NestJS providers and services are injected through the DI container as usual.


Project structure

The layout below is one way to organize a small OPRA NestJS application. There is no single required structure — feature-based layouts, monorepo arrangements, or any other convention work equally well.

src/
├── models/
│ └── customer.ts
├── controllers/
│ └── customers.controller.ts
├── services/
│ └── customers.service.ts
├── app.module.ts
└── main.ts

1. Install dependencies

Install the NestJS transport module along with the core OPRA packages.

npm install @opra/nestjs-http @opra/http @opra/common

2. Define a model

Models are shared between the server and the generated client. Define them once and reference them from controllers, services, and response types.

// src/models/customer.ts
import { ComplexType, ApiField } from '@opra/common';

@ComplexType()
export class Customer {
@ApiField({ type: 'integer' }) declare id: number;
@ApiField({ required: true }) declare name: string;
@ApiField() declare email?: string;
}

3. Define a service (Optional)

NestJS services are injectable providers. Separating data access from controller logic keeps handlers thin and your codebase testable.

// src/services/customers.service.ts
import { Injectable } from '@nestjs/common';
import { Customer } from '../models/customer.js';

@Injectable()
export class CustomersService {
private readonly db: Customer[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob' },
];

findById(id: number) {
return this.db.find(c => c.id === id);
}

findAll() {
return this.db;
}
}

4. Define a controller

OPRA controllers are NestJS providers. @HttpController() applies @Injectable() automatically, so the NestJS DI container resolves constructor dependencies without any extra decoration.

// src/controllers/customers.controller.ts
import { HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';
import { Customer } from '../models/customer.js';
import { CustomersService } from '../services/customers.service.js';

@HttpController('/customers')
export class CustomersController {
constructor(private readonly service: CustomersService) {}

@HttpOperation.GET('/:id', { response: Customer })
async get(ctx: HttpContext) {
return this.service.findById(Number(ctx.pathParams.id));
}

@HttpOperation.GET({ response: [Customer] })
async list(ctx: HttpContext) {
return this.service.findAll();
}
}

5. Set up the module

OpraHttpModule.forRoot() discovers your controllers, builds the ApiDocument, and wires the OPRA middleware into the NestJS application.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { OpraHttpModule } from '@opra/nestjs-http';
import { CustomersController } from './controllers/customers.controller.js';
import { CustomersService } from './services/customers.service.js';
import { Customer } from './models/customer.js';

@Module({
imports: [
OpraHttpModule.forRoot({
name: 'MyApi',
info: { title: 'My API', version: '1.0' },
types: [Customer],
controllers: [CustomersController],
providers: [CustomersController, CustomersService],
basePath: '/api',
schemaIsPublic: true,
}),
],
})
export class AppModule {}

6. Bootstrap NestJS

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('Listening on http://localhost:3000');
}

bootstrap();

7. Test it

curl http://localhost:3000/api/customers/1
curl http://localhost:3000/api/customers
curl http://localhost:3000/api/$schema

Next steps

  • Core Concepts — understand the schema model in depth
  • NestJS — full NestJS integration guide