Skip to main content

Getting Started

1. Define a data type

Services are typed against an OPRA ComplexType. Decorate your class with @ComplexType and annotate each field:

import { ComplexType, ApiField } from '@opra/common';

@ComplexType()
export class Customer {
@ApiField() id: number;
@ApiField() name: string;
@ApiField() email: string;
@ApiField() status: 'active' | 'inactive';
@ApiField() createdAt: Date;
}

2. Create a service class

Extend the service that matches your data shape and pass the data type to the constructor:

import { SqbCollectionService } from '@opra/sqb';
import { Customer } from '../models/customer.js';

export class CustomersService extends SqbCollectionService<Customer> {
constructor() {
super(Customer);
}
}

The table name defaults to the data type name ('Customer'). Override it when needed via the SQB entity metadata on your model class.


3. Connect a database

Pass a SqbClient instance through the options or attach it at runtime via for(context). A common pattern is to inject it through the constructor:

import { SqbClient } from '@sqb/connect';

export class CustomersService extends SqbCollectionService<Customer> {
constructor(readonly db: SqbClient) {
super(Customer, { db });
}
}

4. Use for(context) per request

Every service exposes a for(context, overwriteProperties?) method that returns a scoped copy of the service bound to the current execution context. Call it at the start of every request handler:

async getCustomer(ctx: HttpContext) {
return this.customersService.for(ctx).get(ctx.pathParams.id);
}

for() is cheap — it does not create a new class instance, only a shallow proxy that inherits all configuration from the parent.

You can override any property per-request:

const svc = this.customersService.for(ctx, { scope: 'admin' });

5. NestJS integration

import { Injectable } from '@nestjs/common';
import { SqbClient } from '@sqb/connect';

@Injectable()
export class CustomersService extends SqbCollectionService<Customer> {
constructor(readonly db: SqbClient) {
super(Customer, { db });
}
}

Inject into a controller:

@Injectable()
export class CustomersController {
constructor(private readonly customers: CustomersService) {}

@HttpOperation.Entity.Get({ type: Customer })
async get(ctx: HttpContext) {
return this.customers.for(ctx).get(ctx.pathParams.id);
}
}

Singleton services

For tables that always hold exactly one row, use SqbSingletonService. No key argument is ever needed:

import { SqbSingletonService } from '@opra/sqb';
import { AppSettings } from '../models/app-settings.js';

@Injectable()
export class AppSettingsService extends SqbSingletonService<AppSettings> {
constructor(readonly db: SqbClient) {
super(AppSettings, { db });
}
}

// Usage
const settings = await this.appSettings.for(ctx).get();
await this.appSettings.for(ctx).update({ maintenanceMode: true });

The default singleton id is 1. Override it with { id: yourId } in the constructor options.


Next steps

  • Querying — filter, projection, sort, pagination
  • Mutations — create, update, delete patterns
  • Lifecycle Hooks — validation, audit logging, computed fields