Express
Overview
The @opra/http package provides ExpressAdapter — a platform adapter that integrates an OPRA ApiDocument into an Express application. The adapter registers all controller routes on the Express router, handles request decoding and validation, dispatches to your operation handlers, and serializes the response — all driven by the type declarations you made in your schema.
Installation
npm install @opra/http express
npm install --save-dev @types/express
Setup
Create your ApiDocument, pass it together with an Express Application instance to ExpressAdapter, then start the server.
import express from 'express';
import { ExpressAdapter } from '@opra/http';
import { ApiDocumentFactory } from '@opra/common';
import { CustomersController } from './api/customers.controller.js';
const document = await ApiDocumentFactory.createDocument({
info: { title: 'Customer API', version: '1.0' },
api: {
transport: 'http',
name: 'CustomerApi',
url: '/api',
controllers: [CustomersController],
},
});
const app = express();
const adapter = new ExpressAdapter(app, document, {
basePath: '/api',
});
app.listen(3000, () => {
console.log('Listening on http://localhost:3000');
});
ExpressAdapter registers all routes onto the Express router automatically. No manual app.get() / app.post() calls are needed.
Shutdown
Call adapter.close() to release controller instances on graceful shutdown:
process.on('SIGTERM', async () => {
await adapter.close();
process.exit(0);
});
Adapter Options
| Option | Type | Default | Description |
|---|---|---|---|
basePath | string | '/' | URL prefix for all OPRA routes. |
scope | string | '*' | — | Validation scope applied to every request and response. |
interceptors | (InterceptorFunction | IHttpInterceptor)[] | [] | Interceptor chain executed on every request. |
→ Full API: ExpressAdapter
HttpContext
Every operation handler receives an HttpContext as its first argument. It provides decoded and validated access to all parts of the incoming request.
import { HttpContext } from '@opra/http';
async create(ctx: HttpContext) {
const body = await ctx.getBody<Customer>(); // decoded request body
const id = ctx.pathParams.customerId; // path parameter
const limit = ctx.queryParams.limit; // query string
const token = ctx.headers['authorization']; // request header
const session = ctx.cookies['session']; // cookie value
}
Reading the request body
Use ctx.getBody<T>() to read and decode the request body. The type parameter is for TypeScript inference only — the actual decode is driven by the RequestContent type declared on the operation.
→ Full API: HttpContext
@(HttpOperation.POST()
.RequestContent(Customer)
.Response(201, { type: Customer }))
async create(ctx: HttpContext) {
const body = await ctx.getBody<Customer>();
return this.service.create(body);
}
Reading multipart uploads
Use ctx.isMultipart to check for a multipart request, then obtain a MultipartReader with ctx.getMultipartReader(). The reader streams the incoming multipart/form-data body and exposes each part as either a field (kind: 'field') or a file (kind: 'file'). Fields are decoded and validated against the type declared in .Field() before they are returned; files are written to the OS temp directory as LocalFile instances.
getAll() — collect everything at once
Best for endpoints where the total upload size is bounded and you need all parts before processing.
@(HttpOperation.POST('/:id/documents')
.MultipartContent(
{ maxFiles: 5, maxFileSize: 10_485_760 },
content => content
.File('file', { required: true })
.Field('description', { type: String }),
)
.Response(201))
async uploadDocuments(ctx: HttpContext) {
const reader = await ctx.getMultipartReader();
const parts = await reader.getAll();
for (const part of parts) {
if (part.kind === 'file') {
// part.field — declared field name, e.g. 'file'
// part.filename — original file name sent by the client
// part.storedPath — absolute path to the temp file on disk
// part.type — MIME type, e.g. 'application/pdf'
// part.size — file size in bytes
// part.buffer() — reads the file into a Buffer
// part.text() — reads the file as a UTF-8 string
// part.delete() — manually delete the temp file
await processFile(part.storedPath);
}
if (part.kind === 'field') {
// part.field — declared field name, e.g. 'description'
// part.value — decoded value (type validated by declared schema)
console.log(part.field, part.value);
}
}
// Temp files are deleted automatically when the LocalFile is GC'd
// (autoDelete: true by default). Call purge() to delete immediately.
await reader.purge();
}
getNext() — stream one part at a time
Best for large uploads or when you want to start processing a file before all parts have arrived.
@(HttpOperation.POST('/import')
.MultipartContent(
{ maxFileSize: 104_857_600 }, // 100 MB
content => content
.File('csv', { required: true })
.Field('delimiter', { type: String }),
)
.Response(202))
async importCsv(ctx: HttpContext) {
const reader = await ctx.getMultipartReader();
let delimiter = ',';
let part: MultipartReader.Item | undefined;
while ((part = await reader.getNext())) {
if (part.kind === 'field' && part.field === 'delimiter') {
delimiter = part.value;
}
if (part.kind === 'file' && part.field === 'csv') {
// Start streaming the file immediately — other parts may still be arriving
await streamCsvFile(part.storedPath, { delimiter });
await part.delete(); // delete as soon as we are done
}
}
}
getNext() returns undefined when all parts have been consumed. If a required field declared in .MultipartContent() was never sent, getNext() throws a BadRequestError after the stream ends.
→ Full API: MultipartReader · LocalFile
Cancelling the stream
Call reader.cancel() to abort processing — useful when a validation error occurs mid-stream and you do not want to buffer the remaining parts:
const part = await reader.getNext();
if (!isAllowed(part)) {
reader.cancel();
throw new BadRequestError('Upload not permitted');
}
Interceptors
Interceptors work like Express middleware but operate on HttpContext. They are configured once on the adapter and run on every request in the order they are declared.
import { HttpContext } from '@opra/http';
const adapter = new ExpressAdapter(app, document, {
interceptors: [
// Function form
async (ctx: HttpContext, next) => {
const start = Date.now();
await next();
console.log(`${ctx.request.method} ${ctx.request.url} — ${Date.now() - start}ms`);
},
],
});
Use the class form for interceptors that need dependency injection or shared state:
import { IHttpInterceptor, HttpContext } from '@opra/http';
class AuthInterceptor implements IHttpInterceptor {
async intercept(ctx: HttpContext, next: () => Promise<void>) {
const token = ctx.headers['authorization'];
if (!token) throw new UnauthorizedError();
ctx.locals.user = await verifyToken(token);
await next();
}
}
const adapter = new ExpressAdapter(app, document, {
interceptors: [new AuthInterceptor()],
});
Error Handling
Throw any OpraHttpError subclass from an operation handler or interceptor — the adapter catches it and returns the corresponding HTTP status code and error body automatically.
import {
BadRequestError,
NotFoundError,
UnauthorizedError,
ForbiddenError,
ConflictError,
UnprocessableEntityError,
InternalServerError,
} from '@opra/common';
async get(ctx: HttpContext) {
const customer = await this.service.findById(ctx.pathParams.id);
if (!customer) throw new NotFoundError('Customer not found');
return customer;
}
| Class | Status |
|---|---|
BadRequestError | 400 |
UnauthorizedError | 401 |
ForbiddenError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
UnprocessableEntityError | 422 |
InternalServerError | 500 |
Request validation failures (invalid parameters or body) are converted to 400 Bad Request by the adapter before the handler is even called. Response validation failures produce 500 Internal Server Error.
→ Full reference: HTTP Errors
Schema Endpoint
The adapter exposes the full OPRA schema at GET {basePath}/$schema. No configuration is required — this endpoint is registered automatically.
GET /api/$schema → OpraSchema.ApiDocument (JSON)
Use this endpoint to generate client SDKs, feed API explorers, or inspect the running API definition at any time.