Skip to content
SparkusFrameworkPublic

About

✨ The fastest decorator + dependency injection backend framework for TypeScript.

Resources

Stars

10 stars

Watchers

2 watching

Forks

Repository files navigation

Sparkus ✨

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 });

Why Sparkus

  • 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. createApp picks Bun.serve under Bun and node:http under Node. One conformance suite runs on both: the engine never changes your app's behavior.
  • Standard TypeScript. TC39 decorators, no experimentalDecorators, no reflect-metadata. Dependencies come from inject(Service): typed, no string tokens.
  • One schema, everything typed. params, query, body and response schemas type the handler, convert and validate inputs (/users/abc is a 400 naming params.id), and produce the OpenAPI 3.1 document (app.openapi()). What a schema does not declare is dropped: a client cannot add a role to a body, an entity's passwordHash never 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.ts file of src/ at startup (Bun.Glob under 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 init scaffolds AGENTS.md, CLAUDE.md, skills and a check hook; sparkus check --json validates 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 with ctx.signal, health and readiness probes, graceful shutdown that fails readiness first, and OpenTelemetry spans and metrics through @opentelemetry/api without depending on it. Each one is an option, off by default, and costs nothing when off.
  • Zero runtime dependencies, checked in CI.

Production

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

Packages

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

Command line

bunx @sparkus/cli init my-api

Creates 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.

Testing an app

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" }

Developing Sparkus

bun install
bun run test

bun 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.

Sponsors

Sparkus is sponsored by Nemuria.

Nemuria

License

MIT

About

✨ The fastest decorator + dependency injection backend framework for TypeScript.

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages