The fastest decorator + dependency injection backend framework for TypeScript.
Runs on Bun and Node. Zero runtime dependencies. Built to be written by humans and AI agents.
Sponsored by Nemuria
Status: v1 in development (alpha). The API may still change. See ROADMAP.md.
import { Controller, Get, Post, HttpError, createApp, inject, t, type Ctx } from "sparkus";
class UserService {
find(id: number) {
return id === 42 ? { id, name: "Ada" } : undefined;
}
}
const User = t.object({ id: t.integer(), name: t.string() });
const GetUser = { params: t.object({ id: t.integer({ min: 1 }) }), response: User };
const CreateUser = { body: t.object({ name: t.string({ min: 1 }) }), status: 201 };
@Controller("/users")
class UserController {
private readonly users = inject(UserService);
@Get("/:id", GetUser)
get(ctx: Ctx<typeof GetUser>) {
// ctx.params.id is a number, converted and validated; the return type is checked against User.
const user = this.users.find(ctx.params.id);
if (user === undefined) throw HttpError.notFound(`User ${ctx.params.id} not found.`, { code: "USER_NOT_FOUND" });
return user;
}
@Post("", CreateUser)
create(ctx: Ctx<typeof CreateUser>) {
return { id: 1, ...ctx.body }; // ctx.body is { name: string }: validated, unknown fields dropped
}
}
await createApp({ controllers: [UserController] }).listen({ port: 3000 });- Fast by construction. Everything that can be decided at startup is: dependencies are resolved once, validators are compiled to plain functions, and each route's pipeline (guards, interceptors, validation) is composed ahead of time. A route without guards runs its handler directly. On Bun, routes go to
Bun.serve's native router. - One app, two runtimes.
createApppicksBun.serveunder Bun andnode:httpunder Node. One conformance suite runs on both: the engine never changes your app's behavior. - Standard TypeScript. TC39 decorators, no
experimentalDecorators, noreflect-metadata. Dependencies come frominject(Service): typed, no string tokens. - One schema, everything typed.
params,query,bodyandresponseschemas type the handler, convert and validate inputs (/users/abcis a 400 namingparams.id), and produce the OpenAPI 3.1 document (app.openapi()). What a schema does not declare is dropped: a client cannot add aroleto a body, an entity'spasswordHashnever reaches a response. Already on Zod, Valibot or ArkType? Their schemas work too (Standard Schema). - Errors anyone can act on. HTTP errors are
application/problem+json(RFC 9457) with stable codes. Startup errors (SPK1xxx) say what is wrong and how to fix it: dependency cycles show their full path, duplicate routes name both handlers, a 404 in development suggests the route you meant. Reference: docs/errors.md. - No list to keep up to date.
createApp()loads every*.controller.tsfile ofsrc/at startup (Bun.Globunder Bun): add a file, get its routes. Sorted, checked (SPK1020), 2 to 4 ms for 20 controllers, nothing per request. A list still works:createApp({ controllers: [UserController] }). - Built for agents.
sparkus initscaffoldsAGENTS.md,CLAUDE.md, skills and a check hook;sparkus check --jsonvalidates the wiring without starting the server; llms.txt is the whole API on one page. - Production ready, nothing to install. Typed configuration from the environment (
config(), the app refuses to start and lists every missing variable), structured JSON logs with per-module levels and request ids, CORS, security headers, request timeouts withctx.signal, health and readiness probes, graceful shutdown that fails readiness first, and OpenTelemetry spans and metrics through@opentelemetry/apiwithout depending on it. Each one is an option, off by default, and costs nothing when off. - Zero runtime dependencies, checked in CI.
import { config, createApp, t } from "sparkus";
export const AppConfig = config({
DATABASE_URL: t.string({ min: 1 }),
PORT: t.integer({ default: 3000 }),
MODE: t.enum(["live", "sandbox"], { default: "sandbox" }),
}); // inject(AppConfig).DATABASE_URL in a service; a missing variable stops the app with SPK1018
const app = createApp({ // controllers: every src/**/*.controller.ts
logger: { requests: true }, // JSON lines; LOG_LEVEL=info,db=debug
cors: { origin: ["https://app.example.com"], credentials: true },
securityHeaders: true, // CSP, HSTS, nosniff, frame-options...
requestId: true, // x-request-id in, out, and in every log line
health: true, // GET /health, GET /ready (every HealthCheck service)
requestTimeout: 30_000, // 503 and ctx.signal aborted past 30 s
telemetry: { tracer: trace.getTracer("api"), meter: metrics.getMeter("api") },
});
await app.listen({ shutdownDelay: 5_000 }); // on SIGTERM: /ready fails, then drain| Package | What it is |
|---|---|
sparkus |
What you install: the core plus both engines, picked automatically. |
@sparkus/core |
Decorators, DI container, router, compiled pipeline, errors, app.fetch. |
@sparkus/engine-bun |
Engine on Bun.serve native routes. |
@sparkus/engine-node |
Engine on node:http. |
@sparkus/testing |
createTestApp() with overrides and in-memory inject(). |
@sparkus/cli |
The sparkus command (early). |
bunx @sparkus/cli init my-apiCreates a project ready for humans and agents: an example feature with its tests, AGENTS.md (conventions), CLAUDE.md, skills in .claude/skills/ (add an endpoint, add a service, write a test, fix an SPK error) and a hook that runs sparkus check --json after every edit.
| Command | What it does |
|---|---|
sparkus dev [entry] |
Runs the app and restarts it on every change under src/ (our own watcher, JSON events with --json). |
sparkus check [entry] |
Wires the app (DI, routes, schemas) without listening; prints the first error with its code. |
sparkus routes [entry] |
Lists the routes. |
sparkus openapi [entry] |
Prints the OpenAPI 3.1 document generated from the route schemas (also app.openapi()). |
sparkus build [main] |
Bundles the app for production (dist/, --target bun or node). Controllers are found at build time: the bundle starts without scanning anything. In your own build script: plugins: [sparkusPlugin()] from sparkus/build. |
sparkus client [entry] --out src/client.ts |
Generates a typed client from the route schemas: one file, no dependency. createClient({ baseUrl }).user.get({ params: { id: 42 } }) returns the response schema's type; errors throw ApiError with the problem details. |
import { createTestApp } from "@sparkus/testing";
const app = await createTestApp({
...options, // from src/app.ts; controllers found in src/
overrides: [{ provide: UserService, useValue: { find: () => ({ id: "1", name: "Fake" }) } }],
});
const res = await app.inject({ url: "/users/1" });
// res.status === 200, res.json() === { id: "1", name: "Fake" }bun installbun run testbun run test builds every package with bun build, then runs unit tests and the conformance suite on four targets (in memory, Bun engine, Node engine under Bun, a real Node process; set NODE_BIN to pick the Node binary). bun run typecheck checks sources and tests with bun check, bun run check:deps enforces zero runtime dependencies, and bun run bench runs the benchmark.
Sparkus is sponsored by Nemuria.
MIT