@setu-ts/rest-starter
Opinionated plugin composition for building REST APIs with Setu-TS.
Provides a pre-configured set of plugins for building production-ready REST applications, including configuration, logging, validation, security, health checks, metrics, OpenAPI documentation, decorators, database support, and authentication.
Installation
npm install @setu-ts/rest-starter
# Deno: deno add npm:@setu-ts/rest-starter
Or via npm/yarn/pnpm when using the Node-compatible runtime:
npm install @setu-ts/rest-starter
Usage
The starter exports a single factory function createRestApp that returns a fully wired application
with error handling already configured:
import { createRestApp } from '@setu-ts/rest-starter';
const app = createRestApp();
app.router.get('/hello', (ctx) => ctx.response.text('Hello world'));
await app.start({ port: 3000 });
With Options
You can customize plugin configuration through the optional options parameter:
import { createRestApp } from '@setu-ts/rest-starter';
import type { RestStarterOptions } from '@setu-ts/rest-starter';
// Read from wherever the host supplies secrets: `Deno.env.get(...)` on Deno,
// `process.env...` on Node/Bun, or the `env` argument on Cloudflare Workers.
declare const jwtSecret: string;
declare const sessionSecret: string;
const options: RestStarterOptions = {
config: {/* config plugin options */},
logger: {/* logger plugin options */},
validation: {/* validation plugin options */},
httpSecurity: {/* http-security plugin options */},
health: {/* health plugin options */},
metrics: {/* metrics plugin options */},
openapi: {/* openapi plugin options */},
serviceDiscovery: { provider: 'static', services: {/* service endpoint map */} },
decorators: {/* decorator plugin options */},
database: { type: 'memory' },
// Pick any passive strategy; AuthPlugin installs its middleware globally.
auth: { jwt: { secret: jwtSecret } },
session: { secret: sessionSecret },
di: {/* di plugin options */},
realtime: {
websocket: {/* websocket plugin options */},
sse: {/* sse plugin options */},
backplane: {/* realtime-backplane plugin options */},
},
};
const app = createRestApp(options);
serviceDiscovery is optional. When supplied it registers one ServiceDiscoveryPlugin; because the
microservice and full-stack option types extend RestStarterOptions, the same arm composes through
those starters without a second registration path.
Advanced Plugin Composition
For scenarios requiring custom plugin ordering or selective inclusion, use the buildRestPlugins
builder function together with createApplication from the kernel:
import { buildRestPlugins } from '@setu-ts/rest-starter';
import { createApplication } from '@setu-ts/kernel';
const app = createApplication({
plugins: buildRestPlugins({
database: { type: 'memory' }, // provide options object; omit to exclude
auth: {
jwt: { secret: 'test-secret' },
rbac: { roles: {} },
},
}),
});
Options
Swagger UI is self-hosted by default. Install swagger-ui-dist (^5) in the consuming application;
Deno also needs filesystem read permission. All starter families forward openapi:
createRestApp({ openapi: { swagger: { assets: 'cdn' } } }) selects pinned CDN assets. On Workers,
which has no filesystem to self-host from, the starter selects them itself when no Swagger asset
mode is chosen; an explicit choice, such as assets: { baseUrl: '/assets' }, is kept. Omitting the
package while self-hosting fails startup with OptionalDependencyUnavailableError.
RestStarterOptions is one optional field per plugin. Every field is optional and createRestApp()
with no arguments is valid.
Eight fields configure a plugin the starter always registers — omitting one takes that plugin’s defaults:
| Option | Type |
|---|---|
config | ConfigPluginOptions |
logger | LoggerPluginOptions |
validation | ValidationPluginOptions |
httpSecurity | HttpSecurityPluginOptions |
health | HealthPluginOptions |
metrics | MetricsPluginOptions |
openapi | OpenApiPluginOptions |
decorators | DecoratorPluginOptions |
The remaining seven are gated arms — supplying one registers a plugin that is otherwise absent, so the default composition never bundles something unusable:
| Option | Type | Registers when supplied |
|---|---|---|
database | DatabasePluginOptions | DatabasePlugin |
auth | AuthPluginOptions | AuthPlugin; needs one passive strategy and registers authentication middleware globally. |
session | SessionPluginOptions | SessionPlugin |
di | DiPluginOptions | DiPlugin, which changes how every decorated service is constructed |
graphql | GraphqlPluginOptions | GraphqlPlugin |
serviceDiscovery | ServiceDiscoveryPluginOptions | ServiceDiscoveryPlugin |
realtime | RealtimeArm | WebSocket, SSE, and backplane — one per sub-arm |
Included Plugins
| Plugin | Description |
|---|---|
| RuntimePlugin | Core runtime integration |
| ConfigPlugin | Configuration management |
| LoggerPlugin | Structured logging |
| ValidationPlugin | Request/response validation |
| HttpSecurityPlugin | HTTP security headers |
| HealthPlugin | Health check endpoints |
| MetricsPlugin | Application metrics collection |
| OpenApiPlugin | OpenAPI/Swagger documentation |
| DecoratorPlugin | Decorator-based route registration |
| DatabasePlugin | Optional — database access layer |
| AuthPlugin | Optional — authentication middleware |
| DiPlugin | Optional — DI container |
| GraphqlPlugin | Optional — GraphQL endpoint |
| ServiceDiscovery | Optional — service resolution |
| WebSocketPlugin | Optional — WebSocket messaging |
| SsePlugin | Optional — Server-Sent Events |
| RealtimeBackplane | Optional — cross-replica fan-out |
| SessionPlugin | Optional — cookie sessions + form CSRF |
Gated plugins (database, auth, session, di, graphql, serviceDiscovery, and each
realtime sub-arm) are only included when explicitly provided in options.
The session arm
Adds SessionPlugin: cookie-backed sessions under CAPABILITIES.SESSION, and — with csrf — the
synchronizer-token form-CSRF middleware at priority 325, which is the check a
progressive-enhancement <Form> post can satisfy (the stateless Origin/Referer check in
httpSecurity structurally cannot). Running both together is intended.
const app = createRestApp({
session: { secret: Deno.env.get('SESSION_SECRET')!, csrf: {} },
});
It is gated rather than always-on because the secret cannot be defaulted: SessionPlugin throws
during register() without an adequate one, so an always-on arm would stop every application from
booting until it supplied one. A token-authenticated API needs no cookie at all.
The realtime arm
One option groups the three plugins that make a connection-oriented application work. Each sub-arm’s
presence adds exactly its plugin; realtime: {} adds nothing and is not an error.
const app = createRestApp({
realtime: {
sse: {},
websocket: {},
// Without a backplane, rooms and channels reach only clients connected to
// THIS replica, and both plugins log a scaling notice.
backplane: { transport: 'redis', url: 'redis://localhost:6379' },
},
});
The backplane registers at PLUGIN_PRIORITY.HIGH, so it precedes both consumers regardless of
option order. backplane: {} selects the in-process 'memory' transport.
{ transport: 'messaging' } carries frames over whatever broker is registered under
CAPABILITIES.MESSAGING. The REST tier registers no broker, so that combination throws at start()
naming MessagingPlugin — use the microservice starter, which always registers one, or register
MessagingPlugin yourself before starting.
On Node and Bun a WebSocket upgrade needs a real listening server, so under app.inject() the
plugin registers with available: false. The same application code is correct on Deno and Workers.
The di arm
Supplying di registers DiPlugin, and that changes how every decorated service in the
application is constructed: DecoratorPlugin branches on the presence of a container, so each
@Injectable class becomes a container provider honoring its scope. Omitted (the default), those
classes are constructed directly and registered in the kernel’s ServiceRegistry.
A token the container does not hold falls back to the kernel registry, because DiPlugin’s
autoRegister defaults to true. That is what lets @Inject(CAPABILITIES.LOGGER), or any other
framework capability, reach a decorated class. di: { autoRegister: false } turns the fallback off.
On 0.8.0 and earlier the default was false, so write di: { autoRegister: true } there.
That is why it is an arm rather than always-on — a starter app that never asks for it composes exactly as it did before the option existed.
Coming from NestJS
| NestJS | Setu-TS |
|---|---|
@Module({ … }) | A plugin — IPlugin with provides: [CAPABILITIES.X] |
providers: [UserService] | decorators: { services: [UserService] }, or app.register(…) |
@Injectable() | @Injectable({ token, scope }) |
| Constructor injection by type | @Inject(token, …) on the class, one entry per argument |
@Controller('/users') | @Controller('/users') — identical |
@Get() / @Post() | @Get() / @Post() — identical |
@Body() / @Query() | @Params(Body(), Query()) — sources, listed in argument order |
Guard (CanActivate) | @UseGuards(fn), or an auth-plugin guard factory |
Pipe (ValidationPipe) | @ValidateBody(schema) (Zod) |
| Interceptor | @UseInterceptors(fn) |
| Exception filter | @UseFilters(fn), or errorHandler() middleware |
imports: [OtherModule] | ctx.services.get(CAPABILITIES.X) — no plugin imports another |
| DI is required | DI is the optional di arm |
The one difference that will bite you: constructor injection needs an explicit token.
import { Inject, Injectable } from '@setu-ts/decorator-plugin';
import { CAPABILITIES, type ILogger } from '@setu-ts/common';
@Injectable({ token: 'user-service' })
@Inject(CAPABILITIES.LOGGER)
class UserService {
constructor(private logger: ILogger) {}
}
constructor(private db: DatabaseService) cannot work here. Inferring the token from the
parameter’s type requires emitDecoratorMetadata, which Deno does not support — so the type is
simply not available at runtime. This is permanent, not a gap waiting to be filled.
Two consequences follow from the list being positional:
- The Nth entry binds the Nth constructor argument, so reordering the constructor without reordering the tokens misinjects every argument — they are one declaration and move together.
- A list shorter than the constructor leaves the trailing arguments
undefined; a positional list cannot have gaps. Method parameters are separate, and bind with@Params(...).
Workers Portability
All plugins bundled by default in this starter are compatible with Cloudflare Workers (edge
runtime). The default REST starter is fully Workers-portable — every plugin uses only standard Web
APIs (fetch, Request, Response) and has no filesystem or network-socket dependencies. You can
deploy an app built with rest-starter directly to Workers via
export default { fetch: app.fetch }.
The gated arms vary. di, realtime.websocket, realtime.sse, and a 'memory' backplane are all
Workers-portable. A 'redis' backplane is not — it needs a raw socket. A 'messaging' backplane is
portable only if the broker under CAPABILITIES.MESSAGING is.
Multi-instance Restriction + Escape Hatch
The four multi-instance plugins (cache, database, queue, messaging) accept an
options.name parameter that creates a derived capability token. The starter registers one
instance per arm on the bare token (e.g., CAPABILITIES.CACHE). Setting name through a starter
arm moves the plugin off the bare token, which will break any code that resolves the capability
(including health checks and documentation examples).
The starter does not support setting name through its option arms. If you need a second
instance (e.g., a session cache distinct from the default), register it manually after the starter
returns:
import { createRestApp } from '@setu-ts/rest-starter';
import { CachePlugin } from '@setu-ts/cache-plugin';
const app = createRestApp();
app.register(CachePlugin({ name: 'session' }));
This escape hatch works because createRestApp returns an un-started IKernelApplication that
accepts additional registrations.
See Also
Exports
| Export | Kind |
|---|---|
buildRestPlugins | function |
createRestApp | function |
RealtimeArm | interface |
RestStarterOptions | interface |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.