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: 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