AtomForge layered architecture illustration

@atom-forge/laminar · Architecture

Structure without the ceremony.

As a backend grows, its dependencies should become easier to follow—not disappear behind decorators. Laminar brings functional dependency injection and explicit layers to TypeScript without asking you to adopt an entire application framework.

TypeScript Factory functions Dependency injection Explicit layers

What it is

A layer is a boundary, not a black box.

Laminar groups services and modules into layers. Lazy factory functions create the components and receive their dependencies as arguments, keeping application wiring separate from business logic.

The internal(...) pattern distinguishes layer-local implementation details from the API exposed to neighbouring layers. Functional lifecycle hooks let services describe setup and disposal without mandatory class interfaces.

Why choose Laminar?

Ordinary TypeScript

No decorator syntax or reflection metadata is required. Dependencies are visible in the factory wiring, where you can review and debug them.

Types at the composition boundary

TypeScript can check the dependencies you assemble. That helps catch wiring mistakes early; it does not replace runtime validation of external data.

Test the service, not the framework

Passing dependencies into factories makes it natural to substitute a test implementation instead of booting the whole application.

Focused infrastructure

A small architectural layer can fit applications with lightweight startup requirements. Actual startup cost still depends on your services, database clients and deployment.

How it works

  1. Define factories

    Describe each service as a function of configuration and dependencies. Keep the domain behaviour in the returned object.

  2. Compose a layer

    Use makeLayer to wire factories and expose the intended public surface. Keep dependency direction explicit rather than relying on implicit discovery.

  3. Manage the lifecycle

    Compose onInit and onDispose hooks for resources that need setup or cleanup. Factory assembly is synchronous; asynchronous resource readiness still needs deliberate handling.

An honest fit check

Trade-offs, not fine print

A different mental model

Teams used to class-based DI will need to learn factory composition, lazy dependencies and layer boundaries.

Not an all-in-one backend

Laminar does not supply your HTTP server, database integration or GraphQL schema. You choose and connect those tools.

Architecture still needs design

Explicit wiring is not a blanket guarantee against cycles or initialization mistakes. Keep boundaries intentional and test resource lifecycle behaviour.

In practice

A service with a disposal hook

A service factory with a cleanup hook. defineService comes from your own layer definitions; connect is an application-owned dependency. Assembly is synchronous; await init(services) and dispose(services) explicitly.

TypeScript · Illustrative · adapted from the source texts; package API not verified here
import { onDispose } from '@atom-forge/laminar';
import { defineService } from './layers';

export const databaseService = defineService((config, services) => {
  const connection = services.connect(config.database);

  return {
    db: connection,
    ...onDispose(() => connection.close()),
  };
});