TsdkArc LogoTsdkArc

Why

Your application codebases grow, the code become coupled and messy — hard to reuse, hard to share.TsdkArclets you compose modules like building blocks, nest them, and share them across projects.

Each module declares what it needs and what it provides. Then calling start([modules]) resolves the full dependency graph, boots modules in order, and returns a typed context.

Quickstart

import start, { defineModule } from "tsdkarc";

interface ConfigSlice {
  config: { port: number };
}
const configModule = defineModule()({
  name: "config",
  modules: [],
  boot: (ctx) => ctx.set("config", { port: 3000 }),
});

interface ServerSlice {
  server: { listen: () => void };
}
const serverModule = defineModule()({
  name: "server",
  modules: [configModule], // 👈 Declares dependency
  boot(ctx) {
    ctx.set("server", {
      listen: () => {
        console.log(`Running on ${ctx.config.port}`); // 👈 Fully typed
      },
    });
  },
});

// Launch 🚀
const app = await start([serverModule], {
  afterBoot() {
    console.log("The app is running");
  },
  onError(error, ctx, mod) {
    console.log(`${mod.name} error`, error.message);
    // throw error;
  },
});
app.ctx.server.listen(); // Running on 3000

Core Concepts

TermDescription
SliceThe shape a module adds to the shared context ({ key: Type })
ModuleDeclares dependencies, registers values, and optionally tears them down
ContextThe merged union of all slices — fully typed at each module's boundary

API Reference

defineModule

import { defineModule, Module } from 'tsdkarc';

defineModule()({
  name: string;
  description?: string;
  modules: Module[];
  boot?(ctx): void | Promise;
  beforeBoot?(ctx): void | Promise;
  afterBoot?(ctx): void | Promise;
  shutdown?(ctx): void | Promise;
  beforeShutdown?(ctx): void | Promise;
  afterShutdown?(ctx): void | Promise;
})

start

import start, { type Module,  } from 'tsdkarc';

start(modules: Module[], hooks?: {
  beforeBoot?(ctx): void | Promise,
  afterBoot?(ctx): void | Promise,
  beforeShutdown?(ctx): void | Promise,
  afterShutdown?(ctx): void | Promise,
  beforeEachBoot?(ctx): void | Promise,
  afterEachBoot?(ctx): void | Promise,
  beforeEachShutdown?(ctx): void | Promise,
  afterEachShutdown?(ctx): void | Promise,
}): Promise<{ ctx, stop() }>

Dependency Chain

Downstream modules declare upstream modules and get their context fully typed. start() walks the dependency graph and deduplicates — each module boots exactly once.

import start, { defineModule } from "tsdkarc";

interface ConfigSlice {
  config: { port: number; dbUrl: string };
}
const configModule = defineModule()({
  name: "config",
  modules: [],
  boot: (ctx) => ctx.set("config", { port: 3000, dbUrl: "..." }),
});

interface DbSlice {
  db: Pool;
}
const dbModule = defineModule()({
  name: "db",
  modules: [configModule], // ctx.config is typed here
  async boot(ctx) {
    const pool = new Pool({ connectionString: ctx.config.dbUrl });
    ctx.set("db", pool);
  },
});

interface ServerSlice {
  server: http.Server;
}
const serverModule = defineModule()({
  name: "server",
  modules: [configModule, dbModule], // ctx.config + ctx.db typed
  boot(ctx) {
    ctx.set("server", http.createServer(myHandler));
  },
});

const app = await start([serverModule]);
app.ctx.server.listen(app.ctx.config.port);

Patterns

Register anything, not just data

Functions, class instances, and middleware are all valid context values.

import { defineModule } from "tsdkarc";
import { Request, Response, NextFunction } from "express";

interface AuthSlice {
  authenticate: (req: Request, res: Response, next: NextFunction) => void;
}

export const authModule = defineModule()({
  name: "auth",
  modules: [],
  boot(ctx) {
    ctx.set("authenticate", (req, res, next) => {
      if (!req.headers.authorization) return res.status(401).end();
      next();
    });
  },
});

Lifecycle

beforeBoot boot afterBoot [running] beforeShutdown shutdown afterShutdown
HookFiresPurpose
beforeBootonceBefore the first module begins booting
afterBootonceAfter the last module has finished booting — cross-module ctx is ready
beforeShutdownonceBefore the first module begins shutting down
afterShutdownonceAfter the last module has finished shutting down — final cleanup
Per-module — fires once per module, in boot / shutdown order
beforeEachBootper moduleBefore each individual module boots; receives the module as the second argument
afterEachBootper moduleAfter each individual module finishes booting; receives the module as the second argument
beforeEachShutdownper moduleBefore each individual module shuts down; receives the module as the second argument
afterEachShutdownper moduleAfter each individual module finishes shutting down; receives the module as the second argument