npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@setu-ts/full-stack-starter

v0.11.0

Published

createFullStackApp — the microservice set plus caching, CQRS, storage, and SSR

Readme

@setu-ts/full-stack-starter

Opinionated plugin composition for building full-stack applications with Setu-TS.

The most comprehensive starter bundle, combining REST capabilities, microservice patterns, and full-stack features including caching, event-driven architecture, CQRS, scheduling, auditing, secrets management, storage, mail delivery, feature flags, notifications, multi-tenancy, and React SSR support.

Installation

npm install @setu-ts/full-stack-starter
# Deno: deno add npm:@setu-ts/full-stack-starter

Or via npm/yarn/pnpm:

npm install @setu-ts/full-stack-starter

Usage

The starter exports createFullStackApp — a fully wired application with all enterprise-grade plugins pre-configured:

import { createFullStackApp } from '@setu-ts/full-stack-starter';

const app = createFullStackApp();

app.router.get('/hello', (ctx) => ctx.response.text('Hello world'));

await app.start({ port: 3000 });

With Options

Configure every plugin through the optional options parameter:

import { createFullStackApp } from '@setu-ts/full-stack-starter';
import type { FullStackStarterOptions } from '@setu-ts/full-stack-starter';

const options: FullStackStarterOptions = {
  // REST base plugins (see rest-starter)
  config: {/* ... */},
  logger: {/* ... */},

  // Microservice additions (see microservice-starter)
  messaging: {/* ... */},
  queue: {/* ... */},

  // Full-stack additions
  cache: {/* cache plugin options */},
  events: {/* events plugin options */},
  cqrs: {/* cqrs plugin options */},
  scheduler: {/* scheduler plugin options */},
  audit: {/* audit plugin options */},
  secrets: {/* secrets plugin options */},
  storage: {/* storage plugin options */},
  mail: {/* mail plugin options */},

  // Gated arms (only included when provided)
  featureFlags: { provider: 'memory' },
  notifications: { channels: {} },
  multiTenancy: { resolver: 'header' },
  reactRouter: { serverBuildPath: './build/server/index.js' },
  static: { root: './build/client', urlPrefix: '/assets' },
};

const app = createFullStackApp(options);

Advanced Plugin Composition

Use buildFullStackPlugins together with createApplication from the kernel to construct a custom plugin array for advanced scenarios requiring selective inclusion or different ordering:

import { buildFullStackPlugins } from '@setu-ts/full-stack-starter';
import { createApplication } from '@setu-ts/kernel';

const app = createApplication({
  plugins: buildFullStackPlugins({
    cache: {}, // provide options object; omit to use default memory store
    events: {}, // provide options object; omit to use in-memory bus default
    reactRouter: { serverBuildPath: './build/server/index.js' },
    // Omit featureFlags, notifications, multiTenancy if not needed
  }),
});

Included Plugins

| Category | Plugin | Description | | ---------------- | ------------------ | ------------------------------ | | REST Base | RuntimePlugin | Core runtime integration | | | ConfigPlugin | Configuration management | | | LoggerPlugin | Structured logging | | | ValidationPlugin | Request validation | | | HttpSecurityPlugin | Security headers | | | HealthPlugin | Health check endpoints | | | MetricsPlugin | Metrics collection | | | OpenApiPlugin | OpenAPI documentation | | | DecoratorPlugin | Decorator-based routing | | | DatabasePlugin | Optional database access | | | AuthPlugin | Optional authentication | | Microservice | MessagingPlugin | Async message bus support | | | QueuePlugin | Background job queueing | | | ResiliencePlugin | Circuit breaker & retries | | | TelemetryPlugin | Tracing & observability | | Full-Stack | CachePlugin | Distributed caching | | | EventsPlugin | Event publishing/subscribing | | | CqrsPlugin | CQRS pattern support | | | SchedulerPlugin | Scheduled task execution | | | AuditPlugin | Audit trail logging | | | SecretsPlugin | Secure secret management | | | StoragePlugin | Object/file storage | | | MailPlugin | Email delivery | | | FeatureFlagsPlugin | Dynamic feature toggles | | | NotificationPlugin | Push/notification service | | | MultiTenancyPlugin | Tenant isolation | | | ReactRouterPlugin | React SSR & file-based routing |

OpenApiPlugin self-hosts Swagger UI by default, so install swagger-ui-dist (^5) in the consuming application, or pass openapi: { swagger: { assets: 'cdn' } }. Without either, startup fails with OptionalDependencyUnavailableError. On Cloudflare Workers, which has no filesystem to self-host from, the starter selects the CDN itself when no Swagger asset mode is chosen.

The inherited auth arm accepts any AuthPluginOptions with at least one passive strategy (jwt, apiKey, session, or strategies). When present, AuthPlugin installs authentication middleware globally at priority 300 unless auth.middleware disables or moves it.

Gated plugins (featureFlags, notifications, multiTenancy, reactRouter, static) are only registered when explicitly provided in options. The static arm exists because this is the one tier that by definition serves a browser: supplying it registers StaticPlugin with the given options (typically root: './build/client' so hashed assets and public/ files are served beside SSR); omitting it registers nothing, so the default composition stays byte-identical to before the option existed. A root-level urlPrefix claims the bare wildcard and would collide with the SSR catch-all — give static files their own prefix.

Workers Portability

With default options the starter boots on Cloudflare Workers. Every always-on plugin defaults to an arm that needs no socket or filesystem: memory messaging, queue and storage, log mail, and environment secrets. One plugin is different: SchedulerPlugin refuses Workers at startup, because its timers do not survive isolate eviction, so the starter leaves it out there. Schedule on Workers with Cron Triggers (cloudflare-plugin's WorkersCron). Passing a scheduler arm registers the plugin anyway, and it then fails with an error that names that remedy.

The memory arms are process-local, and a Workers isolate is short-lived, so they carry nothing between isolates. For queues and messaging that must reach another isolate or service, use cloudflare-plugin's Queues-backed WorkersQueue and WorkersBroker. Arms that need sockets or a filesystem (SMTP mail, local storage, the Redis and broker drivers) do not run on Workers.

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, CAPABILITIES.MESSAGING). 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, or a separate queue for dead-letter processing), register it manually after the starter returns:

import { createFullStackApp } from '@setu-ts/full-stack-starter';
import { CachePlugin } from '@setu-ts/cache-plugin';
import { QueuePlugin } from '@setu-ts/queue-plugin';

const app = createFullStackApp();
app.register(CachePlugin({ name: 'session' }));
app.register(QueuePlugin({ name: 'dead-letter' }));

This escape hatch works because createFullStackApp returns an un-started IKernelApplication that accepts additional registrations.

Realtime and DI arms

createFullStackApp inherits the realtime, di, and optional serviceDiscovery arms from the REST starter — the option type extends RestStarterOptions, so every sub-arm behaves identically and nothing new is registered by default.

const app = createFullStackApp({
  di: {},
  serviceDiscovery: { provider: 'static', services: {} },
  realtime: { sse: {}, websocket: {}, backplane: { transport: 'messaging' } },
});

{ transport: 'messaging' } works on this tier without extra wiring, because the microservice set it composes from always registers MessagingPlugin.

None of these arms collide with the plugins this tier already bundles — sse, websocket, di, and the backplane are registered by no other arm. di: {} builds a container that falls back to the kernel registry, so decorated classes can inject framework capabilities; see Constructor injection in loaders.

The session arm is inherited the same way: session: { secret, csrf: {} } adds cookie sessions and the form-CSRF middleware, which a server-rendered <Form> post needs.

See rest-starter for the full description of each arm.

Constructor injection in loaders

React Router builds its loaders and actions itself, so the container never constructs them. A loader reaches an injected service the same way it reaches the session: populateLoadContext resolves it for each request and puts it on a context key the loader reads.

import { Inject, Injectable } from '@setu-ts/decorator-plugin';
import { CAPABILITIES, type IContainer, type ILogger } from '@setu-ts/common';
import { createFullStackAppFromConfig } from '@setu-ts/full-stack-starter';
import { contextKeyFor } from '@setu-ts/react-router-plugin';

@Injectable({ token: 'pricing-service', scope: 'singleton' })
@Inject(CAPABILITIES.LOGGER)
export class PricingService {
  constructor(private readonly logger: ILogger) {}
  quote(cents: number): string {
    this.logger.debug('pricing: quote');
    return `$${(cents / 100).toFixed(2)}`;
  }
}

// Declare it beside the template's other keys in app/lib/context-keys.server.ts:
// contextKeyFor reuses one key per name; separate { defaultValue: null } objects would not.
export const pricingContext = contextKeyFor<PricingService | null>('app.pricing', null);

const app = await createFullStackAppFromConfig(() => ({
  di: {},
  decorators: { services: [PricingService] },
  reactRouter: {
    serverBuildPath: new URL('./build/server/index.js', import.meta.url).href,
    populateLoadContext: (ctx, context) => {
      const container = ctx.services.get<IContainer>(CAPABILITIES.DI_CONTAINER);
      context.set(pricingContext, container.resolve<PricingService>('pricing-service'));
    },
  },
}));

await app.start({ port: 3000 });

A loader then calls context.get(pricingContext)?.quote(4900).

Three things to know:

  • di: {} falls back to the kernel registry. DiPlugin's autoRegister defaults to true, which is what lets @Inject(CAPABILITIES.LOGGER) find the framework's own services. Earlier releases defaulted it to false, so write di: { autoRegister: true } there, or every page answers 500 with No provider registered for DI token 'logger'.
  • Each request has a DI scope. container.forRequest(ctx) returns the request's scope, created on first use and disposed when the response is finished, so a scoped service resolved through it is one instance per request. Resolving a scoped service on the root container throws ScopeRequiredError.
  • A DI mistake in a resolve of your own fails every page, not startup. Since 0.11.0 the container refuses a broken graph at start() — a missing dependency, a cycle, or a singleton that captures a scoped service. But a token that only populateLoadContext resolves is not part of any graph: a misspelled token there, an unlisted service, or a missing di arm starts cleanly and answers /health with 200, then throws on every server-rendered request. The smoke test setu new generates requests / as well as /health, so the project's own test script catches it; keep that request in.

Composing from configuration

Plugin options must be decided before the plugins are constructed — which is before ConfigPlugin has registered anything, so ctx.services.get(CAPABILITIES.CONFIG) is not available yet. createFullStackAppFromConfig closes that ordering gap for every option at once:

import {
  createFullStackAppFromConfig,
  fullStackConfigOf,
  type FullStackStarterOptions,
} from '@setu-ts/full-stack-starter';

const app = await createFullStackAppFromConfig(
  (config): FullStackStarterOptions => ({
    // `prismaClient` is generated and constructed by the application — a Prisma v7
    // client carries its own connection configuration, so `url` is not an option.
    database: { type: 'prisma', options: { prismaClient } },
    session: { secret: config.getOrThrow<string>('SESSION_SECRET'), csrf: {} },
    messaging: {
      broker: 'redis-streams',
      url: config.get<string>('REDIS_URL', { default: 'redis://127.0.0.1:6379' }),
    },
    queue: {
      adapter: 'redis',
      url: config.get<string>('REDIS_URL', { default: 'redis://127.0.0.1:6379' }),
    },
  }),
  { config: { envFilePath: ['.env.local', '.env'] } },
);

const config = fullStackConfigOf(app); // the exact snapshot passed to the callback

await app.start({ port: 3000 });

The Redis options above require ioredis installed in the application. The CLI supplies that client and these options with setu new portal --template full-stack --broker redis --queue redis, or from a workspace's --transport redis when generating a full-stack member. The same config read sees dotenv and deployment environment values; omitting the options keeps the starter's memory defaults.

Configuration is loaded once and the same snapshot is registered under CAPABILITIES.CONFIG, so the values the composition branched on are the values handlers read — not a second snapshot taken a moment later. The resolver runs exactly once; if it throws, or configuration fails to load, the promise rejects and no partially-composed application exists.

Post-factory code reads that same object with fullStackConfigOf(app). Calling the accessor for an application built by another factory throws FullStackConfigUnavailableError.

This is why no plugin option carries a config-key shorthand such as urlFromConfig: that field would need its value at the same impossible moment. Secrets are further out of reach — they are served by secrets-plugin after registration, so a plugin needing one resolves it lazily at use time.

The React Router app skeleton

This package supplies the plugin composition; it cannot supply the application's app/ directory, because a JSR library cannot write files into your project. Scaffold that with the CLI, which generates a setu.config.ts calling createFullStackAppFromConfig:

setu new my-app --template full-stack

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(...).

See Also

Exports

| Export | Kind | | --------------------------------- | --------- | | buildFullStackPlugins | function | | createFullStackApp | function | | createFullStackAppFromConfig | function | | fullStackConfigOf | function | | FullStackConfigUnavailableError | class | | FromConfigOptions | interface | | FullStackStarterOptions | interface | | RealtimeArm | interface | | StaticPluginOptions | type |

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.