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
Define modules, reuse the modules, and run application.
import start, { defineModule, type ContextOf, type ContextWriterOf, type SetOf } from "tsdkarc"; // interface ConfigSlice { // config: { port: number }; // } // const configModule = defineModule<ConfigSlice>()({ const configModule = defineModule()({ name: "config", boot: () => ({ config: { port: 3000, }, }), }); // Get the module's context type(include the dependencies modules) export type ConfigModuleCtx = ContextOf<typeof configModule>; // same as `ConfigSlice` // Get the `set` type of the module type ConfigModuleSet = ContextWriterOf<typeof configModule>["set"]; // or type ConfigModuleSet2 = SetOf<typeof configModule> function test(set: ConfigModuleSet) { // type safe here set("config", { port: 200 }); } interface ServerSlice { server: { listen: () => void }; } const serverModule = defineModule<ServerSlice>()({ name: "server", modules: [configModule] as const, // 👈 Declares dependency boot(ctx) { return { server: { listen: () => { console.log(`Running on ${ctx.config.port}`); // 👈 Fully typed }, }, }; // Or: /* 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 |
Online Playground
API Reference
defineModule
import { defineModule, Module, type ContextOf, type SetOf,type ContextWriterOf } from 'tsdkarc'; defineModule() defineModule<OwnSlice>()({ name: string; description?: string; modules: Module[]; boot?(ctx): OwnSlice | Promise<OwnSlice> | void | Promise<void>, beforeBoot?(ctx): void | Promise<void>; afterBoot?(ctx): void | Promise<void>; shutdown?(ctx): void | Promise<void>; beforeShutdown?(ctx): void | Promise<void>; afterShutdown?(ctx): void | Promise<void>; }) type ModuleContext = ContextOf<Module>
start
import start, { type Module, } from 'tsdkarc'; start(modules: Module[], hooks?: { beforeBoot?(ctx): void | Promise<void>, afterBoot?(ctx): void | Promise<void>, beforeShutdown?(ctx): void | Promise<void>, afterShutdown?(ctx): void | Promise<void>, beforeEachBoot?(ctx, module): void | Promise<void>, afterEachBoot?(ctx, module): void | Promise<void>, beforeEachShutdown?(ctx, module): void | Promise<void>, afterEachShutdown?(ctx, module): void | Promise<void>, onError?(error, ctx, module): void | Promise<void>, }): 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<ConfigSlice>()({ name: "config", boot: (ctx) => ctx.set("config", { port: 3000, dbUrl: "..." }), }); interface DbSlice { db: Pool; } const dbModule = defineModule<DbSlice>()({ 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<ServerSlice>()({ 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<AuthSlice>()({ name: "auth", boot(ctx) { ctx.set("authenticate", (req, res, next) => { if (!req.headers.authorization) return res.status(401).end(); next(); }); }, });
Lifecycle
| 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 |
| onError | Once | When resolve modules and hooks error will call `onError`, default will throw |
| 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 |
