@macss/modular-api
v0.4.0
Published
Use-case-centric toolkit for building modular APIs with Express. Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and expose Swagger/OpenAPI documentation automatically.
Downloads
411
Maintainers
Readme
modular_api
Use-case centric toolkit for building modular APIs with Express.
Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and get automatic Swagger/OpenAPI documentation.
Also available in Dart: modular_api
Quick start
import { ModularApi, ModuleBuilder } from 'modular_api';
// ─── Module builder (separate file in real projects) ──────────
function buildGreetingsModule(m: ModuleBuilder): void {
m.usecase('hello', HelloWorld.fromJson);
}
// ─── Server ───────────────────────────────────────────────────
const api = new ModularApi({ basePath: '/api' });
api.module('greetings', buildGreetingsModule);
api.serve({ port: 8080 });curl -X POST http://localhost:8080/api/greetings/hello \
-H "Content-Type: application/json" \
-d '{"name":"World"}'{ "message": "Hello, World!" }Docs → http://localhost:8080/docs
Health → http://localhost:8080/health
OpenAPI JSON → http://localhost:8080/openapi.json (also /openapi.yaml)
Metrics → http://localhost:8080/metrics (opt-in)
See example/example.ts for the full implementation including Input, Output, UseCase with validate(), and the builder.
Features
UseCase<I, O>— pure business logic, no HTTP concernsInput/Output— DTOs withtoJson()andtoSchema()for automatic OpenAPIOutput.statusCode— custom HTTP status codes per responseUseCaseException— structured error handling (status code, message, error code, details)ModularApi+ModuleBuilder— module registration and routing- Constructor-based unit testing with fake dependency injection
cors()middleware — built-in CORS support- Swagger UI at
/docs— auto-generated from registered use cases - OpenAPI spec at
/openapi.jsonand/openapi.yaml— raw spec download - Health check at
GET /health— IETF Health Check Response Format - Prometheus metrics at
GET /metrics— Prometheus exposition format - Structured JSON logging — Loki/Grafana compatible, request-scoped with trace_id
- All endpoints default to
POST(configurable per use case) - Full TypeScript declarations (
.d.ts) included
Installation
npm install @macss/modular-apiError handling
async execute() {
const user = await repository.findById(this.input.userId);
if (!user) {
throw new UseCaseException({
statusCode: 404,
message: 'User not found',
errorCode: 'USER_NOT_FOUND',
});
}
this.output = new GetUserOutput(user);
}{ "error": "USER_NOT_FOUND", "message": "User not found" }Testing
Write true unit tests by injecting fake dependencies directly through the constructor. No HTTP server or real infrastructure needed.
import { describe, it, expect, beforeEach } from 'vitest';
import { UseCaseException } from 'modular_api';
// ─── Fake ────────────────────────────────────────────────────
class FakeGreetingRepository implements GreetingRepository {
saved: string[] = [];
async save(name: string): Promise<void> {
this.saved.push(name);
}
}
// ─── Tests ───────────────────────────────────────────────────
describe('SayHello', () => {
let fakeRepo: FakeGreetingRepository;
beforeEach(() => {
fakeRepo = new FakeGreetingRepository();
});
it('greets correctly', async () => {
const usecase = new SayHello(new SayHelloInput('World'), { repository: fakeRepo });
expect(usecase.validate()).toBeNull();
const output = await usecase.execute();
expect(output.message).toBe('Hello, World!');
expect(fakeRepo.saved).toContain('World');
});
it('rejects empty name', () => {
const usecase = new SayHello(new SayHelloInput(''), { repository: fakeRepo });
expect(usecase.validate()).not.toBeNull();
});
it('throws UseCaseException when repo fails', async () => {
const failingRepo = {
save: async () => {
throw new Error('DB error');
},
};
const usecase = new SayHello(new SayHelloInput('World'), { repository: failingRepo });
await expect(usecase.execute()).rejects.toThrow(UseCaseException);
});
});For integration tests against real infrastructure, use UseCase.fromJson() directly
(no helper wrapper needed):
it('integration — end to end with real DB', async () => {
const usecase = SayHello.fromJson({ name: 'World' });
await usecase.execute();
expect(usecase.output.message).toBe('Hello, World!');
});See doc/testing_guide.md for the full guide.
Architecture
HTTP Request → ModularApi → Module → UseCase → Business Logic → Output → HTTP Response- UseCase layer — pure logic, independent of HTTP
- HTTP adapter — turns a UseCase into an Express RequestHandler
- Middlewares — cross-cutting concerns (CORS, logging)
- Swagger UI — documentation served automatically
License
MIT © ccisne.dev
