Blog / Architecture

Building a Unified API Abstraction Layer for Enterprise Connectors

When you have one enterprise API integration, a per-connector client is fine. When you have ten, the architecture matters. The decision you make at connector two or three determines whether you end up with ten independently-maintained clients or one coherent abstraction that scales gracefully.

This article walks through the design of a unified API abstraction layer: the interface decisions, the auth layer, the pagination layer, and where the abstraction leaks. This is the architecture Devloom implements as a product — but the design principles apply whether you're building it yourself or evaluating an SDK.

Why Build an Abstraction Layer

The cost of per-connector clients is not the initial implementation — it's ongoing maintenance. Consider what you have when you've implemented ten separate connectors in the naive approach:

  • Ten different auth implementations, each handling token refresh slightly differently
  • Ten different pagination loops, each encoding the specific pagination model of that API
  • Ten different retry implementations, some correct, some not
  • Ten different TypeScript response types, none of them interoperable

When you need to change how your system handles 429 errors — because you discovered the naive implementation is causing thundering herd problems — you change it in ten places. When you add an 11th connector, you write all of this again.

An abstraction layer solves the multiplication problem: infrastructure changes once, downstream code stays stable, new connectors are additive rather than multiplicative.

Interface Design

The core constraint on the interface is that it must be narrow enough to be implementable across diverse connectors, but expressive enough to be useful without constant escape-hatch usage. Too narrow and every real integration requires .raw(); too wide and the interface is a leaky specification that different connectors implement inconsistently.

The minimal viable interface for read-heavy enterprise API integrations:

interface ConnectorClient {
  query<T>(resource: string): QueryBuilder<T>;
  stream(event: string): StreamSubscription;
  mutate<T>(resource: string, data: Partial<T>): Promise<T>;
  raw(method: string, path: string, options?: RequestInit): Promise<Response>;
}

interface QueryBuilder<T> {
  filter(predicates: Record<string, unknown>): this;
  select(fields: string[]): this;
  limit(n: number): this;
  autopage(): this;
  all(): Promise<T[]>;
  first(): Promise<T | null>;
  count(): Promise<number>;
}

The raw() escape hatch is not optional. You will encounter connector-specific operations that don't fit the normalized interface — bulk operations, custom reports, non-standard event types. Providing raw() with the client's auth and retry already applied means you get the infrastructure benefits without being forced to use a per-connector client for edge cases.

Auth Layer

The auth layer needs to handle three patterns (OAuth2, API key, JWT) behind a single interface. The key insight is that from the abstraction layer's perspective, all three reduce to the same operation: "give me a valid authorization header for the next request."

interface AuthProvider {
  getAuthHeader(): Promise<Record<string, string>>;
}

// Each auth strategy implements AuthProvider
class OAuth2Provider implements AuthProvider {
  async getAuthHeader() {
    const token = await this.getValidToken();
    return { 'Authorization': `Bearer ${token}` };
  }
}

class ApiKeyProvider implements AuthProvider {
  async getAuthHeader() {
    return { [this.headerName]: this.key };
  }
}

The connector client calls auth.getAuthHeader() before every request. The specific strategy — whether it needs to check expiry, refresh a token, or sign a JWT — is encapsulated in the strategy class. The connector client doesn't branch on auth type.

Pagination Layer

The pagination layer is the most complex part of the abstraction. The interface is simple (.autopage()) but the implementations are diverse. Each pagination strategy is a class that implements a common interface:

interface PaginationStrategy {
  hasNextPage(response: ApiResponse): boolean;
  getNextPageParams(response: ApiResponse): Record<string, string>;
}

The QueryBuilder's internal page fetcher calls strategy.hasNextPage() after each response and, if true, calls strategy.getNextPageParams() to build the next request. The CursorStrategy reads response.next_cursor. The OffsetStrategy increments the offset by the page size. The PageTokenStrategy forwards response.nextPageToken.

Error Normalization

Each connector throws different error shapes. The abstraction layer should normalize these to a common error hierarchy:

  • DlxAuthError — 401/403, credentials invalid or expired
  • DlxRateLimitError — 429, quota exceeded
  • DlxNotFoundError — 404, resource doesn't exist
  • DlxServerError — 5xx, upstream failure
  • DlxNetworkError — connection failure, timeout

Downstream code catches DlxAuthError and knows what to do (re-authorize) without needing to know whether the upstream API returned { error: 'invalid_token' }, { code: 'AUTH_FAILED' }, or a 401 with an empty body.

Tradeoffs and When to Not Build This

A unified abstraction layer is the right choice when you have three or more connectors with similar data models, or when you anticipate adding more connectors over time. It's the wrong choice when you have one or two highly specialized connectors with unique operations that don't generalize — the abstraction overhead exceeds the value in those cases.

The maintenance cost is real: every time you add a new connector, you implement two or three strategy classes and register the connector in the factory. This is lower cost than a full per-connector client, but it's not zero. If your team isn't disciplined about maintaining the strategy registry, connectors accumulate undocumented quirks in their strategy implementations that undermine the abstraction.

For teams that don't want to maintain the abstraction layer themselves, Devloom is this architecture as a hosted service: we maintain the connector strategies, you call the normalized interface.