Skip to content

Architecture & Design Patterns

This document describes the architectural principles, component lifecycle, and design patterns that govern the Karin framework. It is intended for developers who require a deep understanding of the framework's internal mechanics, dependency resolution strategies, and platform-agnostic capabilities.

Runtime Architectures

Karin supports two distinct runtime modes. This duality allows the framework to adapt its bootstrapping process to the specific constraints of the deployment environment.

1. File-Based Auto-Discovery (Monolith Mode)

Ideal for: Bun (Native), Long-running processes (Docker, Kubernetes).

In this mode, the framework utilizes filesystem traversal to automatically locate and register components. This prioritizes Developer Experience (DX) by eliminating the need for manual module registration. Note: This mode is currently optimized for the Bun runtime.

  • Mechanism: usage of Bun.Glob to locate files, followed by dynamic import() and Reflect.getMetadata analysis.
  • Startup Lifecycle: Scanner -> Importer -> Metadata Resolution -> DI Container Registration -> Route Binding.
  • Benefit: Exceptional Developer Experience (DX). Creating a file immediately registers the route without manual intervention.
typescript
// main.ts
import { KarinFactory } from "@project-karin/core";
import { H3Adapter } from "@project-karin/platform-h3";

async function bootstrap() {
  const app = await KarinFactory.create(new H3Adapter(), {
    // recursively scan and register all controllers
    scan: "./src/**/*.ts",
  });
  app.listen(3000);
}

2. Explicit Registration (Serverless/Edge Mode)

Ideal for: Cloudflare Workers, Deno Deploy.

In serverless environments, I/O operations (filesystem scanning) are costly. To achieve fast cold starts, Karin bypasses the discovery phase entirely.

  • Mechanism: Components are imported statically (ESM) and passed explicitly to the factory. This allows bundlers (esbuild, rollup, webpack) to perform static analysis and Tree-Shaking, removing all unused code from the final bundle.
  • Startup Lifecycle: Static Import Resolution -> DI Container Registration -> Route Binding.
  • Benefit: Deterministic dependency graph and minimal bootstrap latency (often <5ms).
typescript
// index.ts
import { KarinFactory } from "@project-karin/core";
import { H3Adapter } from "@project-karin/platform-h3";
import { UsersController } from "./users/users.controller";
import { AuthService } from "./auth/auth.service";

// "Serverless" factory skips scanning and uses provided arrays
const app = KarinFactory.serverless(new H3Adapter(), {
  controllers: [UsersController],
  providers: [AuthService], // Service is manually registered
  scan: false,
});

export default app;

Core Components and Separation of Concerns

Karin enforces a strict layered architecture. Each layer has a distinct responsibility, ensuring that the application remains testable and maintainable as it scales.

1. Controllers (Presentation Layer)

The Controller is the entry point for all incoming HTTP requests. Its primary responsibility is to act as an interface between the external world (HTTP) and the internal application domain.

A Controller is responsible for:

  • Routing: Mapping specific HTTP Methods (GET, POST, etc.) and paths (e.g., /users/:id) to handler methods.
  • Deserialization: Converting raw HTTP data (body, query params, headers) into structured objects or DTOs (Data Transfer Objects).
  • Validation: Ensuring the integrity of the incoming data before it reaches the business logic (often using Pipes).
  • Orchestration: Delegating the actual work to one or more Services.
  • Serialization: Transforming the result from the Service into a standard HTTP response (setting Status Codes, Headers, etc.).

Crucially, Controllers should be "Thin". They must not contain business logic, complex conditionals, or direct database queries. A Controller's method should generally be a linear flow: Validate -> Delegate -> Return.

Concept: The Waiter

Think of the Controller as a Waiter. Their job is to take the customer's order, validate it (e.g., "we're out of soup"), and hand it to the kitchen. A waiter never cooks the food themselves.

2. WebSockets (Gateways / Presentation Layer)

While Controllers handle discrete HTTP requests, Gateways handle persistent, bidirectional connections. They serve as the presentation layer for real-time applications.

A Gateway is responsible for:

  • Event Handling: Mapping incoming WebSocket events to specific methods.
  • Connection Management: Dealing with the lifecycle of a connection (Connect/Disconnect).
  • Broadcasting: Sending messages to specific clients or groups (Rooms).
  • Integration: Using the same Guards, Pipes, and Interceptors as HTTP controllers.

Concept: The Live Operator

If the Controller is a Waiter taking one order at a time, the Gateway is a Live Operator maintaining an open line to multiple clients simultaneously.

3. Services (Business Logic Layer)

Services are the heart of the application. They encapsulate the business rules, domain logic, and data manipulation operations.

A Service is responsible for:

  • Data Persistence: Interacting with the database via an ORM (Prisma, Drizzle, etc.) or Ref repositories.
  • Integration: Communicating with external systems (Payment Gateways, CRMs, Email Providers).
  • Computation: Executing algorithms, data transformations, or complex calculations.
  • Bypassing HTTP: Since Services are decoupled from the HTTP layer, they can be reused in any Context: a CLI command, a WebSocket event, or a background Cron job can all invoke the same UserService.create() method.

Services are typically designed as Singletons managed by the Dependency Injection container.

Concept: The Chef

The Service is the Chef. They have the recipes/logic and tools to prepare the dish. They don't care who placed the order (CLI or HTTP); they simply execute the task and return the result.

4. Dependency Injection (The IoC Container)

As applications grow, managing dependencies manually (e.g., new UserService(new DbService(new ConfigService()))) becomes unmanageable and creates tight coupling between classes. Karin utilizes Inversion of Control (IoC) to solve this.

  • Registration: Classes decorated with @Service or @injectable are registered in a central DICache.
  • Resolution: When a class requests a dependency in its constructor (e.g., constructor(private userService: UserService)), the container identifies resolution rules, instantiates the required dependency (or retrieves an existing singleton), and injects it.
  • Lifecycle: The container manages the lifecycle of these instances, including initialization (onModuleInit) and destruction (onModuleDestroy).

Concept: The Kitchen Manager

The DI Container acts as the Kitchen Manager. It ensures that when a new station is opened, all necessary tools and staff (dependencies) are already there and ready to work, so the staff doesn't have to hire their own assistants.

5. Adapters (Abstraction Layer)

Karin is Platform Agnostic. The core framework interacts with HTTP requests through an abstract interface (IHttpAdapter), never directly with the underlying server implementation.

this allows the same application code to run on distinctly different runtimes:

  • H3Adapter: Uses the h3 library (native to Nuxt/Nitro). It is lightweight, standard-compliant, and highly performant. Ideal for standard Bun applications.
  • HonoAdapter: Uses hono. It offers broader ecosystem support (middleware) and is highly optimized for edge runtimes like Cloudflare Workers or Deno Deploy.

The Request Lifecycle

When a request hits a Karin application, it flows through a defined pipeline of components. Understanding this flow is critical for debugging and middleware implementation.

1. Incoming Request

The Platform Adapter receives the raw request (Node IncomingMessage or Web Standard Request) and normalizes it.

2. Global Middleware

Global middleware functions execute. These catch the request before it matches any route (e.g., CORS handling, global logging, security headers).

3. Routing

The RouterExplorer matches the URL and Method to a registered Controller Handler.

4. Guards (Authentication & Authorization)

  • Question: "Is this request allowed?"
  • Guards execute to verify identity (JWT verification) or permissions (Role checks).
  • If a Guard returns false, the pipeline aborts immediately with an HTTP 403 Forbidden.

5. Interceptors (Pre-Controller)

  • Question: "Do we need to transform the context?"
  • Interceptors run before the handler. They can start performance timers, mutate the request object, or manage database transactions.

6. Pipes (Validation)

  • Question: "Is the data valid?"
  • Pipes validate and transform the arguments passed to the controller method (e.g., ZodValidationPipe ensures the body matches a DTO).
  • If validation fails, the pipeline aborts with an HTTP 400 Bad Request.

7. Route Handler (Execution)

The distinct method in your Controller is executed. This is where your code calls the necessary Services.

8. Interceptors (Post-Controller)

The return value from the handler flows back up through the Interceptors. They can transform the response (e.g., wrapping the data in a standardized { data: T, meta: ... } response structure).

9. Exception Filters (Error Handling)

If any application logic throws an error, it is caught here. Filters examine the error type and normalize it into a user-friendly JSON response, preventing the server from crashing or leaking stack traces to the client.

10. Response Transmission

The Adapter serializes the final result and sends it back to the client.


Bootstrapping Process

The initialization of a Karin generic application follows a strict sequence to ensure stability:

  1. Environment Normalization: (Serverless only) process.env and global contexts are polyfilled if missing.
  2. Metadata Reflection: The framework reads all @decorators to understand the dependency graph.
  3. Discovery: Files are scanned (in Auto mode) or registered from arrays (in Manual mode).
  4. Dependency Resolution: The IoC container builds the Class tree, instantiating Singletons from the leaves up to the root.
  5. Lifecycle Hooks: The onModuleInit() method is called for all providers, allowing for DB connections or cache warm-ups.
  6. Route Mapping: The RouterExplorer iterates through all controllers and registers paths with the underlying Adapter.
  7. Server Listen: The HTTP server binds to the port and begins accepting traffic.

Released under the MIT License.