If Primitives are the "superpowers" you attach to classes, Providers are the engines that power them.
In strict architectural terms, a Service contains your Business Logic (the "What"), and a Provider contains the Infrastructure Logic (the "How").
When writing an application, you often need to talk to the outside world: sending emails, connecting to Redis, uploading files to S3.
You could put this logic directly inside your User Service, but that makes testing a nightmare. Instead, Alepha encourages you to wrap this infrastructure code into a Provider.
At the end, providers are just a convention to make mocking and swapping implementations easier.
A Provider is just a class. However, it typically does three things:
$env.$hook (connect/disconnect).Here is a production-ready Email Provider:
1import { $env, $hook, t } from "alepha"; 2import { $logger } from "alepha/logger"; 3import { createTransport, type Transporter } from "my-super-mailer-lib"; 4 5export class EmailProvider { 6 log = $logger(); 7 8 // 1. Configuration: The provider validates its own requirements 9 env = $env(t.object({10 SMTP_HOST: t.text(),11 SMTP_USER: t.text(),12 SMTP_PASS: t.text(),13 }));14 15 transporter = createTransport({16 host: this.env.SMTP_HOST,17 auth: { user: this.env.SMTP_USER, pass: this.env.SMTP_PASS }18 });19 20 // 2. Lifecycle: Connect when the app starts21 onStart = $hook({22 on: "start",23 handler: async () => {24 await this.transporter.verify();25 this.log.info("Connected to SMTP");26 }27 });28 29 // 3. Public API: The rest of your app uses this30 async send(to: string, subject: string) {31 return this.transporter.sendMail({ from: "me@app.com", to, subject });32 }33}
Injecting a provider is no different than injecting a service. Use $inject.
1import { $inject, $action } from "alepha"; 2import { EmailProvider } from "./EmailProvider"; 3 4class UserService { 5 // Alepha injects the singleton instance of EmailProvider 6 email = $inject(EmailProvider); 7 8 register = $action({ 9 handler: async ({ body }) => {10 // ... logic to create user ...11 await this.email.send(body.email, "Welcome!");12 }13 });14}
This is where Alepha shines.
Sometimes, you want the "What" to stay the same, but the "How" to change depending on where the app is running.
You can achieve this using Service Substitution.
This defines the "Contract". Your services will depend on this.
1export abstract class QueueProvider {2 abstract push(job: object): Promise<void>;3}
Different ways to fulfill the contract.
1// For Production: Real Redis 2export class RedisQueueProvider extends QueueProvider { 3 async push(job: object) { /* ... redis logic ... */ } 4} 5 6// For Dev/Test: Just an array in memory 7export class MemoryQueueProvider extends QueueProvider { 8 queue: object[] = []; 9 async push(job: object) { this.queue.push(job); }10}
In your entry point, you tell Alepha which implementation to use based on the environment.
1import { Alepha, run } from "alepha"; 2import { QueueProvider, RedisQueueProvider, MemoryQueueProvider } from "./queue"; 3 4const alepha = Alepha.create(); 5 6// The Magic: Dependency Injection wiring 7if (alepha.isProduction()) { 8 // In Prod: When someone asks for 'QueueProvider', give them 'RedisQueueProvider' 9 alepha.with({ provide: QueueProvider, use: RedisQueueProvider });10} else {11 // In Dev: When someone asks for 'QueueProvider', give them 'MemoryQueueProvider'12 alepha.with({ provide: QueueProvider, use: MemoryQueueProvider });13}14 15// Your app logic doesn't care. It just injects 'QueueProvider'.16run(alepha);
Voilà, now you are an Alepha EXPERT.
Alepha provides standard implementations for common needs so you don't have to reinvent the wheel.
alepha/bucket: File storage. Switches between LocalFileStorageProvider (disk) and S3/Azure automatically.alepha/queue: Job queues. Switches between MemoryQueue and RedisQueue.alepha/logger: Logging. Switches between Console (pretty colors) and JSON (for log aggregators).You focus on the logic; Alepha handles the plumbing.