NJ Municipality Lookup
CodebaseSrc

Adapters

Implementations of domain ports, connecting the application to external services and infrastructure. Adapters translate between domain interfaces and specific t

Adapters Layer (adapters/)

Implementations of domain ports, connecting the application to external services and infrastructure. Adapters translate between domain interfaces and specific technologies.

Purpose

Adapters implement the port interfaces defined in domain/ports/, providing concrete integrations with:

  • External APIs (NJ Geocoding Service)
  • Caching systems (in-memory LRU/LFU cache)
  • HTTP clients (fetch-based with retry logic)

Structure

  • cache/ - Cache service implementations (in-memory, factory for multiple types)
  • nj-api/ - NJ Office of GIS geocoding API client
  • http/ - HTTP client with timeout and retry capabilities

Adapter Implementations

Cache Adapters (cache/)

  • in-memory-cache.ts - High-performance in-memory cache with hybrid LRU/LFU eviction
  • cache-factory.ts - Factory for creating cache instances (in-memory, Redis placeholder, no-op)

API Adapters (nj-api/)

  • geocoding-client.ts - Implements GeocodingServicePort for NJ geocoding API
  • suggestions-client.ts - Implements SuggestionsServicePort for address autocomplete
  • endpoint-builder.ts - Constructs API URLs with query parameters
  • api-config.ts - API endpoints and configuration constants

HTTP Adapters (http/)

  • fetch-client.ts - Implements HttpClientPort with exponential backoff retry logic

Key Principles

Adapter Pattern

Adapters translate between domain concepts and external APIs:

// Domain port interface
interface GeocodingServicePort {
  geocodeAddress(address: AddressInput): Promise<GeocodingResult>;
}

// Adapter implements port, calls external API
export class NJGeocodingClient implements GeocodingServicePort {
  async geocodeAddress(address: AddressInput): Promise<GeocodingResult> {
    const apiResponse = await this.httpClient.get<NJApiResponse>(url);
    return this.transformToGeocodingResult(apiResponse.data); //Translation
  }
}

Dependency Injection

Adapters receive their own dependencies:

// Adapter receives HTTP client (another adapter)
export const createNJGeocodingClient = (
  httpClient: HttpClientPort, //Injected dependency
): GeocodingServicePort => {
  // Implementation
};

Error Transformation

Adapters catch external errors and throw domain errors:

try {
  const response = await fetch(url);
} catch (error) {
  // Transform infrastructure error → domain error
  throw new ApiTimeoutError({ url, timeoutMs });
}

Configuration

Adapters are configured through lib/config.ts:

const config = {
  njApi: {
    baseUrl: process.env.NJ_API_BASE_URL,
    timeoutMs: 10000,
    retryEnabled: true,
    maxRetries: 3,
  },
  cache: {
    maxEntries: 16384,
    ttlDays: 7,
  },
};

Testing

Adapters are tested at multiple levels:

  1. Unit Tests: Isolated with mocked dependencies
  2. Integration Tests: Real HTTP calls to external APIs (marked with .integration.test.ts)
  3. Contract Tests: Verify adapters satisfy port interfaces

Adding New Adapters

  1. Create adapter module in appropriate subdirectory
  2. Implement one or more domain port interfaces
  3. Add configuration to lib/config.ts
  4. Write unit tests with mocked dependencies
  5. Write integration tests for real external calls
  6. Document adapter in this README

On this page