Skip to main content
Open Platform for Rich APIs

Meet OPRA.

A schema-first TypeScript framework for building REST APIs — with a fully typed client generated automatically from your server definition.

One source. Zero duplication.

Define your API shape once in TypeScript. OPRA derives validation, documentation, and the client contract from the same model — no spec files to keep in sync.

If it compiles, it works.

Every parameter, field, and return type flows from your controller definition to the generated client. The TypeScript compiler becomes your integration test.

Everything talks to everything.

Server, client, Angular module, CLI, testing utilities — each package was designed with the others in mind. No adapter glue, no impedance mismatch.

Define once. Use everywhere.

Write your controller on the server. Run oprimp. Get a type-safe client ready to use in any TypeScript project.

Server — controller definition
import { HttpController, HttpOperation } from '@opra/common';
import { HttpContext } from '@opra/http';

@HttpController({ description: 'Customers collection' })
export class CustomersController {

@(HttpOperation.Entity.FindMany(Customer)
.SortFields('_id', 'givenName', 'familyName')
.Filter('givenName', ['=', 'like'])
.Filter('familyName', ['=', 'like']))
async findMany(ctx: HttpContext) {
const { limit = 10, skip = 0 } = ctx.queryParams;
return this.service.findMany({ limit, skip });
}

@HttpOperation.Entity.Create(Customer)
async create(ctx: HttpContext) {
const body = await ctx.getBody<Customer>();
return this.service.create(body);
}
}
Client — generated & typed
// Auto-generated, fully typed client
const api = new CustomerApi(httpClient);

// List customers
const list = await api.$customers
.findMany({ limit: 10, sort: ['familyName'] })
.getBody();

// Get by id
const customer = await api.$customer
.get(42)
.getBody();

// Create
const created = await api.$customers
.create({ givenName: 'Jane', familyName: 'Doe' })
.getBody();

Everything you need

Your TypeScript IS your schema.

Define a class, decorate it, done. No JSON Schema files, no YAML specs, no code-gen step. The model you write is the single source of truth for validation, serialization, and documentation.

Garbage in, clean data out.

Incoming values are coerced to their declared type first — strings become numbers, missing booleans default to false — then validated. Bad data never reaches your business logic.

Swap databases without touching your API.

OPRA's service layer abstracts your data source. Switch from MongoDB to SQL, add Elasticsearch on top, or connect a custom backend — the controller and the client don't notice.

NestJS

Already have NestJS? You're halfway there.

OPRA controllers and services are first-class NestJS citizens. No parallel module system, no special bootstrap — drop them in alongside your existing providers.

The client writes itself.

Run oprimp against your running API once. Get a fully typed TypeScript client back — every endpoint, every field, every filter option, all inferred. No manual maintenance, ever.

/** */

Your IDE already knows the answer.

Descriptions, defaults, and constraints you define on the server end up as JSDoc on the generated client. Hover over a method and the full API contract appears inline.

One model. Everything else follows.

Most frameworks ask you to describe your API in multiple places. OPRA doesn't.

The usual way
With OPRA

Define a TypeScript model, write a separate OpenAPI spec, add a validation library, and keep all three in sync.

Define a TypeScript model. Schema, validation rules, and documentation are derived automatically — nothing to keep in sync.

Handwrite an HTTP client. Update it every time an endpoint, field, or type changes.

Run oprimp once. Get a fully typed client. Re-run when things change. That's it.

Parse query params, headers, and body manually. Wire up a multipart parser separately for file uploads.

Every part of the request — parameters, headers, body, and file uploads — is parsed, coerced, and validated before your handler runs.

Implement pagination, filtering, sorting, and field projection by hand — for every entity, every data source.

Extend a base service class. Pagination, filtering, sorting, and projection are built in — just connect your data source.

Build separate implementations for HTTP, Socket.io, Kafka, RabbitMQ — different patterns, different abstractions, different maintenance burden.

Define your service once. OPRA runs it over HTTP, WebSocket, Kafka, RabbitMQ, or any other transport — same model, same validation, same contract.

Write documentation separately. Watch it drift the moment the code changes.

Docs are derived from the server definition. They're always correct — because they can't not be.

Ready to start?

Follow the guide and have your first API running in minutes.

Read the docs →