alepha@docs:~/docs/concepts$
cat 4-modules.md
2 min read
Last commit:

#Modules

When you are building a "Hello World" app, you can put everything in main.ts. When you are building a SaaS platform, that approach turns into spaghetti code before lunch.

Modules are Alepha's tool for organizing code into logical, decoupled domains. They allow you to build a Modular Monolith: an application that runs as a single process but is structured like a set of independent microservices.

#Defining a Module

A module is simply a container that groups related Services, Providers, and Primitives. You define one using the $module primitive.

ts
 1import { $module } from "alepha"; 2import { UserController } from "./UserController"; 3import { UserRepository } from "./UserRepository"; 4  5export const UserModule = $module({ 6  // Namespaces are important for logging and debugging 7  name: "app.users", 8  9  // Register all services belonging to this domain10  services: [11    UserController,12    UserRepository13  ]14});

Then, you register the module in your main entry point.

ts
1import { Alepha, run } from "alepha";2import { UserModule } from "./modules/users";3import { BillingModule } from "./modules/billing";4 5const app = Alepha.create()6  .with(UserModule)7  .with(BillingModule);8 9run(app);

#The Golden Rule: Boundaries

Here is where Alepha differs from other frameworks.

In a traditional Node.js app, it is tempting to just import BillingService into UserService and use it. Don't do this.

If Module A directly $injects a service from Module B, you have created a tight coupling. You cannot split them later. You cannot deploy them separately. You have created a "Distributed Monolith" worst-case scenario.

#How to communicate between modules

Alepha enforces a strict boundary using the Links system.

Instead of injecting the instance of another module's service, you create a Client that talks to its API surface.

ts
 1import { $client } from "alepha/server/links"; 2import type { NotificationApi } from "../notifications/NotificationApi"; 3  4class UserService { 5  // ❌ BAD: Tight coupling 6  // notifications = $inject(NotificationService); 7  8  // ✅ GOOD: Loose coupling via API contract 9  notifications = $client<NotificationApi>();10 11  createUser = $action({12    handler: async ({ body }) => {13      // Create the user...14 15      // Call the other module16      // -> if both modules are local, this is a direct call17      // -> if remote, this is an remote call18      await this.notifications.sendWelcomeEmail({ email: body.email });19    }20  });21}

#Why do it this way?

  1. Zero-Latency Local Calls: When both modules are running in the same process (the Monolith), Alepha detects this. The $client call effectively becomes a direct function call. There is no HTTP overhead.
  2. Location Transparency: If your app grows and you decide to move NotificationModule to a separate server (Microservices), you don't change a single line of code in UserService. You just configure via $remote for the discovery.
  3. Type Safety: You still get full TypeScript autocomplete and validation for the remote module, because TypeBox schemas are shared.

#Summary

Feature Use $inject Use $client
Scope Inside the same module. Across different modules.
Performance Direct memory reference. Optimized proxy (direct call or HTTP).
Coupling High (Dependencies are hard-linked). Low (Contract-based communication).
Use Case Service logic, Database Repositories. communicating between domains (e.g., Billing -> Users).

Think of Modules as "Mini Applications" living together. Treat them with respect, keep their boundaries clean, and your codebase will remain maintainable for years.

On This Page
No headings found...
ready
mainTypeScript
UTF-8concepts_modules.md