Skip to content

tsdk-monorepo/tsdkarc

Repository files navigation

TsdkArc: the Elegant, fully type-safe module composable library TsdkArc

TsdkArc: the Elegant, fully type-safe module composable library
The Elegant, Fully Type-safe Module Composable Library.

npm version Size 0 dependencies PRs Welcome GitHub license jsDocs.io typescript

Why TsdkArc

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

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

Quick Start

npm install tsdkarc
import start, {
  defineModule,
  type ContextOf,
  type ContextWriterOf,
  type SetOf,
} from "tsdkarc";

// interface ConfigSlice {
//   config: { port: number; env: string };
// }

// const configModule = defineModule<ConfigSlice>()({
const configModule = defineModule()({
  // or return ctx from boot, no need <ConfigSlice>
  name: "config",
  boot(ctx) {
    return {
      config: {
        env: process.env.NODE_ENV ?? "development",
        port: Number(process.env.PORT) || 3000,
      },
    };
    // Or:
    /* 
      ctx.set("config", {
        env: process.env.NODE_ENV ?? "development",
        port: Number(process.env.PORT) || 3000,
      });
    */
  },
});

// Get the module's context type(include the dependencies modules)
type ConfigModuleCtx = ContextOf<typeof configModule>; // same as `ConfigSlice`

// Get the `set` type of the module
type ConfigModuleSet = ContextWriterOf<typeof configModule>["set"];
type ConfigModuleSet = SetOf<typeof configModule>;

// Run
(async () => {
  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;
    },
  });
  console.log(app.ctx.config.port); // 3000
  await app.stop();
})();

Core Concepts

Term Description
Slice The shape a module adds to the shared context ({ key: Type })
Module Declares dependencies (modules), registers values (ctx.set), and optionally tears them down
Context The merged union of all slices — fully typed at each module's boundary

API Outline

// OwnSlice type is optional, it can auto infer the type from `boot()`'s return
defineModule<OwnSlice>()({
  name: 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>,
})

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): void | Promise<void>,
  afterEachBoot?(ctx): void | Promise<void>,
  beforeEachShutdown?(ctx): void | Promise<void>,
  afterEachShutdown?(ctx): void | Promise<void>,

  onError?(error: Error, ctx, mod): void | Promise<void>,
}): Promise<{ ctx, stop() }>

Dependency Chain

Downstream modules declare upstream modules and get their context fully typed.

const dbModule = defineModule()({
  name: "db",
  modules: [configModule] as const, // ctx.config is typed here
  async boot(ctx) {
    const pool = new Pool({ connectionString: ctx.config.databaseUrl });
    await pool.connect();
    // ctx.set("db", pool);
    return { db: pool };
  },
  async shutdown(ctx) {
    await ctx.db.end();
  },
});

const serverModule = defineModule()({
  name: "server",
  modules: [configModule, dbModule] as const, // ctx.config + ctx.db both typed
  boot(ctx) {
    // ctx.set("server", http.createServer(myHandler));
    return { server: http.createServer(myHandler) };
  },
  afterBoot(ctx) {
    ctx.server.listen(ctx.config.port);
  },
  shutdown(ctx) {
    ctx.server.close();
  },
});

const app = await start([serverModule]);
await app.stop();

start() walks the dependency graph and deduplicates — each module boots exactly once regardless of how many times it appears in modules arrays.


Global Hooks

Use the second argument to start() for process-level concerns that need access to the full context.

const app = await start([serverModule], {
  afterBoot(ctx) {
    process.on("uncaughtException", (err) => console.error(err));
  },
  beforeShutdown(ctx) {
    console.info("shutting down");
  },
  afterEachBoot(ctx) {
    console.info("booting", mod.name);
  },
  beforeEachShutdown(ctx, mod) {
    console.info("shutting down", mod.name);
  },
});

Patterns

Register anything, not just data. Functions, class instances, and middleware are all valid context values.

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

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

Compose small modules. Each module is independently testable and replaceable. Complex wiring is just a chain of dependencies.

// config → db → cache → queue → server
const app = await start([serverModule]);

Lifecycle

beforeBoot → boot → afterBoot → [running] → beforeShutdown → shutdown → afterShutdown
Hook Purpose
beforeBoot Pre-setup, Called once before the first module begins booting.
boot Register values on ctx via return ctx or call ctx.set()
afterBoot Called once after the last module has finished booting.
beforeShutdown Called once before the first module begins shutting down.
shutdown Release resources
afterShutdown Called once after the last module has finished shutting down.
beforeEachBoot Called before each individual module boots, in boot order.
afterEachBoot Called after each individual module finishes booting, in boot order.
beforeEachShutdown Called before each individual module shuts down, in shutdown order.
afterEachShutdown Called after each individual module finishes shutting down, in shutdown order.

API

defineModule<Slice>()(config)

Field Type Description
name string Unique module identifier
description string Optional description
modules Module<Slice>[] Declared dependencies
beforeBoot (ctx) => void | Promise Runs before boot
boot (ctx) => void | Promise Register values on ctx
afterBoot (ctx) => void | Promise Runs after all modules booted
beforeShutdown (ctx) => void | Promise Runs before shutdown
shutdown (ctx) => void | Promise Release resources
afterShutdown (ctx) => void | Promise Runs after shutdown

start(roots, options?){ ctx, stop }

Field Description
roots Top-level modules (deps auto-resolved)
options Global lifecycle hooks
ctx Merged, fully-typed context
stop Triggers full shutdown sequence

Projects You May Also Be Interested In

  • xior - A tiny but powerful fetch wrapper with plugins support and axios-like API
  • tsdk - Type-safe API development CLI tool for TypeScript projects
  • broad-infinite-list - ⚡ High performance and Bidirectional infinite scrolling list component for React and Vue3
  • littkk - 🧞‍♂️ Shows and hides UI elements on scroll.

Reporting Issues

Found an issue? Please feel free to create issue

Support

If you find this project helpful, consider buying me a coffee.