Skip to main content

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 consume public-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:
Service throws → Interceptor catches → Filter formats → Response sent
This keeps business logic clean and centralizes error handling at the infrastructure boundary.

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:
LayerComponentInput → Output
PresentationSortPipe, FilterPipeREST strings → Domain types
ApplicationMemberServiceDomain types (framework-agnostic)
InfrastructureGraphQLFieldMapperDomain types → GraphQL format

Example: Sort Translation

REST:     sort=-createdAt,status

Domain:   [{ field: 'createdAt', order: 'DESC' }, { field: 'status', order: 'ASC' }]

GraphQL:  [{ field: 'CREATED_AT', order: 'DESC' }, { field: 'STATUS', order: 'ASC' }]

Example: Filter Translation

REST:     filter[status]=active

Domain:   [{ field: 'status', operator: 'eq', value: 'active' }]

GraphQL:  [{ field: 'STATUS', operator: 'EQUAL', value: 'active' }]
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
By building external-api this way from day one, we validate the architectural patterns before committing to a full migration. This is incremental modernization done right.

Next Steps

Build Tooling

Consider migrating to esbuild for faster builds and smaller bundles.