Current Approach
The External API is a Backend-for-Frontend (BFF) that exposes member (and other endpoints later) endpoints via REST, connecting to internal GraphQL services.Why Proxy Through public-api?
We deliberately chose to consumepublic-api via GraphQL rather than importing repositories or services from the legacy core.
Problems with Direct Legacy Integration
- Command bus coupling: The legacy core uses a command bus pattern that would force this module to inherit its abstractions, handlers, and event dispatch mechanisms
- Transitive dependencies: Importing a single repository pulls in ORM entities, domain models, services, and configuration from the legacy monolith
- Shared state risks: Direct database access could bypass business rules enforced in the legacy layer
- Migration friction: Any refactoring in the legacy core would immediately break this module
Benefits of the Proxy Approach
- Zero coupling: This module has no imports from legacy code. It’s a completely isolated codebase
- Stable contract: GraphQL schema acts as a versioned API contract. Changes in legacy internals don’t propagate here
- Independent evolution: We can refactor, test, and deploy this module without touching the legacy core
- Future-ready: When domain refactoring happens, we swap the GraphQL adapter for a new repository adapter. The rest of the module remains unchanged
No direct imports from legacy. Ever.
Architecture
We apply Clean Architecture principles through a DDD approach, ensuring clear layer separation and dependency inversion.Key Principles
- Bounded context isolation : Member module owns its gateway interfaces (no coupling to external contract modules)
- Interface Segregation : Member only depends on what it needs from contracts (
MemberContractSummary) - Dependencies point inward : Infrastructure → Application → Domain
- Gateway as ACL : Transforms external contract data to member’s domain model at the boundary
NestJS Patterns
Interceptors for Response Wrapping
Interceptors handle cross-cutting concerns for outgoing responses:- Consistent response envelope formatting
- Logging of request/response cycles
- Error transformation
Pipes for Input Handling
Pipes validate and transform incoming data:- DTO validation via class-validator
- Filter parameter parsing
- Sort parameter parsing
Filters for Exception Handling
Exception filters catch and format errors consistently:- Domain exceptions mapped to appropriate HTTP status codes
- Structured error responses with error codes
Simplified Error Handling
Services don’t implement complex error handling. Instead, we let exceptions propagate naturally through the NestJS pipeline:REST → GraphQL Translation Pipeline
The API exposes REST endpoints with JSON API-style query parameters, while internally communicating with GraphQL. The translation happens through a clean pipe architecture:Translation Flow
Each layer transforms data to the next format:| Layer | Component | Input → Output |
|---|---|---|
| Presentation | SortPipe, FilterPipe | REST strings → Domain types |
| Application | MemberService | Domain types (framework-agnostic) |
| Infrastructure | GraphQLFieldMapper | Domain types → GraphQL format |
Example: Sort Translation
Example: Filter Translation
The
- prefix in sort indicates descending order (JSON API convention). The GraphQL layer handles SCREAMING_CASE conversion.Blueprint for Modular Monolith
This module is not just an isolated API : it’s a proof of concept for our future architecture.The Vision
- Migrate to a modular monolith using a monorepo framework (e.g., Nx)
- Extract bounded contexts into independent domain services
- Each service owns its data (same DB different schema), exposes clean APIs, and has no shared runtime dependencies
Why external-api Leads the Way
- Demonstrates how a module can be fully decoupled while coexisting with legacy code
- Establishes patterns for inter-service communication (API contracts over shared code)
- Proves we can build new features without inheriting technical debt
- Serves as a template for future domain extractions
external-api this way from day one, we validate the architectural patterns before committing to a full migration. This is incremental modernization done right.