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
| Term | Description |
|---|---|
| Slice | The shape a module adds to the shared context ({ key: Type }) |
| Module | Declares dependencies, registers values, and optionally tears them down |
| Context | The 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
| Hook | Fires | Purpose |
|---|---|---|
| beforeBoot | once | Before the first module begins booting |
| afterBoot | once | After the last module has finished booting — cross-module ctx is ready |
| beforeShutdown | once | Before the first module begins shutting down |
| afterShutdown | once | After the last module has finished shutting down — final cleanup |
| Per-module — fires once per module, in boot / shutdown order | ||
| beforeEachBoot | per module | Before each individual module boots; receives the module as the second argument |
| afterEachBoot | per module | After each individual module finishes booting; receives the module as the second argument |
| beforeEachShutdown | per module | Before each individual module shuts down; receives the module as the second argument |
| afterEachShutdown | per module | After each individual module finishes shutting down; receives the module as the second argument |
