Definition

An API gateway is a server that acts as the single entry point for a group of backend services, providing a unified interface for external clients while handling cross-cutting concerns such as authentication, authorization, rate limiting, request routing, load balancing, protocol translation, and response caching. The gateway pattern decouples client-facing API design from internal service architecture, enabling backend services to evolve independently while maintaining a stable external API contract.

API gateways solve several architectural challenges that emerge as applications grow beyond monolithic designs. Without a gateway, each client must know the addresses and APIs of individual backend services, authenticate separately with each service, and handle cross-cutting concerns like retry logic and circuit breaking independently. A gateway centralizes these responsibilities, reducing client complexity and providing a single point for security enforcement, monitoring, and traffic management.

Overview

The pattern has roots in the facade design pattern from object-oriented programming and the integration layer pattern from enterprise application architecture. Modern API gateways range from simple reverse proxies with authentication (Nginx, Caddy) to full-featured API management platforms (Kong, AWS API Gateway, Apigee) with developer portals, analytics, and lifecycle management.

The Prismatic Platform implements a distinctive gateway through the prismatic_api application, which auto-discovers backend functions and exposes them through a unified OpenAPI-documented REST interface. This auto-introspection approach eliminates the common maintenance burden of keeping route configurations synchronized with backend service APIs.

Gateway Evolution

GenerationApproachExampleLimitation
Gen 1Hardware load balancerF5, Citrix NetScalerNo application awareness
Gen 2Reverse proxyNginx, HAProxyLimited routing logic
Gen 3API gatewayKong, ApigeeManual route configuration
Gen 4Service meshIstio, LinkerdHigh operational complexity
PrismaticAuto-introspecting gatewayPrismaticAPIZero manual route management

The Prismatic Platform's approach is unique in Generation 4+: rather than requiring manual route configuration or service mesh infrastructure, the gateway scans all Prismatic* facade modules at boot time, discovers their public functions, maps Elixir type specifications to OpenAPI schemas, and exposes every function as a REST endpoint. This creates a self-documenting, self-configuring API surface.

Technical Deep Dive

Gateway Responsibilities

ResponsibilityDescriptionPrismatic Implementation
Request RoutingDirect requests to appropriate backendURL pattern matching → module/function dispatch
AuthenticationVerify client identityPlug pipeline, Bearer tokens, API keys
AuthorizationEnforce access control policiesRBAC via Casbin, scope validation
Rate LimitingPrevent abuse, enforce quotasToken bucket per-client in ETS
Protocol TranslationConvert between formatsJSON ↔ Elixir terms, auto-coercion
Request ValidationVerify request structureOpenApiSpex schema validation
Response TransformationNormalize response formatConsistent envelope structure
CachingCache frequent responsesETS cache with TTL
MonitoringTrack latency, errors, throughputTelemetry events per-endpoint
Circuit BreakingPrevent cascade failuresPer-backend circuit breaker state
DocumentationGenerate API docsOpenAPI 3.0 + SwaggerUI

Gateway Architectural Patterns

Different gateway patterns serve different architectural needs:

1. Routing Gateway (Simple Proxy)

Routes requests to the correct backend based on URL path or host header. The simplest pattern, adding minimal latency.

flowchart LR
    Client --> Gateway
    Gateway -->|/osint/*| OsintCore[PrismaticOsintCore]
    Gateway -->|/dd/*| Dd[PrismaticDd]
    Gateway -->|/auth/*| Auth[PrismaticAuth]

2. Aggregation Gateway (BFF Pattern)

Combines responses from multiple backend services into a single response, reducing client round-trips. Essential for mobile and low-bandwidth clients.

# Aggregation: combine OSINT results from multiple tools
defmodule PrismaticApi.Aggregator do
  @moduledoc """
  Aggregates responses from multiple backend services
  into a single unified response for the client.
  """

  @spec aggregate_entity_intelligence(String.t()) :: {:ok, map()}
  def aggregate_entity_intelligence(entity_name) do
    tasks = [
      Task.async(fn -> PrismaticOsintCore.search(:czech_ares, %{query: entity_name}) end),
      Task.async(fn -> PrismaticOsintCore.search(:czech_justice, %{query: entity_name}) end),
      Task.async(fn -> PrismaticDd.get_entity_by_name(entity_name) end),
      Task.async(fn -> PrismaticPerimeter.check_domain(entity_name) end)
    ]

    results = Task.await_many(tasks, 15_000)

    {:ok, %{
      ares: Enum.at(results, 0),
      justice: Enum.at(results, 1),
      dd_entity: Enum.at(results, 2),
      perimeter: Enum.at(results, 3),
      aggregated_at: DateTime.utc_now()
    }}
  end
end

3. Offloading Gateway

Handles cross-cutting concerns (auth, logging, rate limiting) while passing requests through with minimal transformation. The backend services remain unaware of these concerns.

4. Translation Gateway

Converts between protocols and data formats. For example, exposing a GenServer-based backend as a REST API, or translating between JSON and internal Elixir terms.

Gateway vs Reverse Proxy vs Service Mesh

FeatureReverse ProxyAPI GatewayService Mesh
ScopeNetwork levelApplication levelInter-service level
RoutingSimple path/hostComplex with transformsService-to-service
AuthBasic (HTTP, IP)Full (JWT, OAuth, RBAC)mTLS, SPIFFE
Rate LimitingBasic or nonePer-client quotasPer-service quotas
API DocsNoneOpenAPI, portalNone
ProtocolHTTP/TCP passthroughFull translationSidecar proxy
MonitoringAccess logsDetailed analyticsDistributed tracing
ComplexityLowMediumHigh
PrismaticNginx (production)PrismaticAPI (app)N/A (monolith)

Plug Pipeline Architecture

The Prismatic Platform's gateway architecture is built on Phoenix's Plug pipeline model. Each cross-cutting concern is implemented as an independent plug:

defmodule PrismaticApi.Router do
  use PrismaticApi, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug PrismaticApi.Plugs.RequestId
    plug PrismaticApi.Plugs.Telemetry
    plug PrismaticApi.Plugs.RateLimiter
    plug PrismaticApi.Plugs.Authentication
    plug PrismaticApi.Plugs.Authorization
    plug PrismaticApi.Plugs.RequestValidation
    plug OpenApiSpex.Plug.PutApiSpec, module: PrismaticApi.ApiSpec
  end

  scope "/api/v1", PrismaticApi do
    pipe_through :api

    # Auto-discovered endpoints
    get "/:app/:action", DispatchController, :dispatch
    post "/:app/:action", DispatchController, :dispatch
  end

  # OpenAPI documentation
  scope "/api" do
    get "/openapi", OpenApiSpex.Plug.RenderSpec, []
    get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
  end
end

Each plug in the pipeline has a single responsibility:

defmodule PrismaticApi.Plugs.RateLimiter do
  @moduledoc """
  Token bucket rate limiter plug.
  Limits requests per API key to prevent abuse.
  """
  @behaviour Plug

  import Plug.Conn

  @max_requests 100
  @window_seconds 60

  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, _opts) do
    client_id = get_client_id(conn)

    case check_rate(client_id) do
      :ok ->
        conn
        |> put_resp_header("x-ratelimit-remaining", to_string(remaining(client_id)))
        |> put_resp_header("x-ratelimit-limit", to_string(@max_requests))

      {:error, :rate_limited} ->
        conn
        |> put_resp_header("retry-after", to_string(@window_seconds))
        |> send_resp(429, Jason.encode!(%{error: "rate_limited"}))
        |> halt()
    end
  end

  defp get_client_id(conn) do
    case get_req_header(conn, "authorization") do
      ["Bearer " <> token] -> token
      _ -> conn.remote_ip |> :inet.ntoa() |> to_string()
    end
  end

  defp check_rate(client_id) do
    now = System.system_time(:second)
    key = {client_id, div(now, @window_seconds)}

    case :ets.update_counter(:api_rate_limits, key, {2, 1}, {key, 0}) do
      count when count <= @max_requests -> :ok
      _ -> {:error, :rate_limited}
    end
  end

  defp remaining(client_id) do
    now = System.system_time(:second)
    key = {client_id, div(now, @window_seconds)}

    case :ets.lookup(:api_rate_limits, key) do
      [{_, count}] -> max(@max_requests - count, 0)
      [] -> @max_requests
    end
  end
end

Auto-Discovery and Introspection

The most distinctive feature of the Prismatic gateway is its auto-discovery system. At boot time, the gateway:

  1. Scans modules: Finds all modules matching Prismatic* that export public functions
  2. Extracts specs: Reads @spec annotations to determine parameter types and return types
  3. Maps to OpenAPI: Converts Elixir typespecs to JSON Schema / OpenAPI 3.0 types
  4. Registers endpoints: Creates route entries in the ETS registry
  5. Generates docs: Builds OpenAPI 3.0 specification from discovered metadata
defmodule PrismaticApi.Scanner do
  @moduledoc """
  Scans Prismatic facade modules and discovers public functions
  for automatic REST API exposure.
  """

  @spec scan_prismatic_modules() :: list(module())
  def scan_prismatic_modules do
    :code.all_available()
    |> Enum.filter(fn {mod, _, _} ->
      mod_string = to_string(mod)
      String.starts_with?(mod_string, "Elixir.Prismatic") and
        not String.contains?(mod_string, "Test") and
        not String.contains?(mod_string, "Web")
    end)
    |> Enum.map(fn {mod, _, _} -> List.to_atom(mod) end)
  end

  @spec extract_functions(module()) :: list(map())
  def extract_functions(module) do
    module.__info__(:functions)
    |> Enum.reject(fn {name, _arity} ->
      name in [:__info__, :module_info, :__struct__]
    end)
    |> Enum.map(fn {name, arity} ->
      %{
        module: module,
        function: name,
        arity: arity,
        doc: get_function_doc(module, name, arity),
        spec: get_function_spec(module, name, arity)
      }
    end)
  end

  @spec build_endpoint_spec(map()) :: map()
  def build_endpoint_spec(%{module: module, function: function} = func_info) do
    app_name = module
      |> Module.split()
      |> Enum.at(0)
      |> Macro.underscore()
      |> String.replace("_", "-")

    %{
      app: app_name,
      action: to_string(function),
      module: module,
      function: function,
      arity: func_info.arity,
      http_method: if(func_info.arity <= 1, do: :get, else: :post),
      param_specs: extract_param_specs(func_info.spec),
      doc: func_info.doc,
      path: "/api/v1/#{app_name}/#{function}"
    }
  end

  defp get_function_doc(module, name, arity) do
    case Code.fetch_docs(module) do
      {:docs_v1, _, _, _, _, _, docs} ->
        Enum.find_value(docs, fn
          {{:function, ^name, ^arity}, _, _, %{"en" => doc}, _} -> doc
          _ -> nil
        end)
      _ -> nil
    end
  end

  defp get_function_spec(_module, _name, _arity), do: nil

  defp extract_param_specs(nil), do: []
  defp extract_param_specs(spec), do: [spec]
end

Dispatch Controller

The dispatch controller resolves URL parameters to module/function pairs and handles the actual invocation:

defmodule PrismaticApi.DispatchController do
  @moduledoc """
  Generic dispatch controller for auto-discovered API endpoints.
  Resolves {app, action} URL parameters to {module, function} pairs,
  validates parameters, and invokes the backend function.
  """
  use PrismaticApi, :controller

  alias PrismaticApi.{Registry, Dispatcher}

  require Logger

  @spec dispatch(Plug.Conn.t(), map()) :: Plug.Conn.t()
  def dispatch(conn, %{"app" => app, "action" => action} = params) do
    start_time = System.monotonic_time()

    with {:ok, endpoint} <- Registry.lookup(app, action),
         {:ok, args} <- Dispatcher.validate_params(endpoint, params),
         {:ok, result} <- Dispatcher.safe_apply(endpoint.module, endpoint.function, args) do

      duration = System.monotonic_time() - start_time

      :telemetry.execute(
        [:prismatic_api, :dispatch, :success],
        %{duration: duration},
        %{app: app, action: action}
      )

      conn
      |> put_status(200)
      |> json(%{data: result, meta: %{app: app, action: action}})
    else
      {:error, :not_found} ->
        conn |> put_status(404) |> json(%{error: "endpoint_not_found"})

      {:error, {:validation_error, details}} ->
        conn |> put_status(400) |> json(%{error: "validation_error", details: details})

      {:error, reason} ->
        Logger.warning("Gateway dispatch error: #{inspect(reason)}")
        conn |> put_status(500) |> json(%{error: "internal_error"})
    end
  end
end

Gateway Security

Authentication Flow

flowchart TD
    A[Client Request] --> B[Extract Token]
    B --> C[Validate Token]
    C --> D[Load Identity]
    D --> E[Check Scopes]
    E --> F[Rate Check]
    F --> G[Dispatch to Backend]

    B -.- B1>"Bearer token or API key from header"]
    C -.- C1>"JWT verification, key lookup"]
    D -.- D1>"Resolve to user/service identity"]
    E -.- E1>"Verify endpoint access permission"]
    F -.- F1>"Per-identity rate limit"]
    G -.- G1>"Route to backend service"]

Common Gateway Security Threats

ThreatMitigationPrismatic Implementation
DDoSRate limiting + WAFToken bucket per-IP/key
InjectionInput validationOpenApiSpex schema validation
Auth bypassCentralized authPlug pipeline, no bypass path
Data leakageResponse filteringExplicit field selection
Replay attackNonce/timestampRequest signing (optional)
SSRFURL validationAllowlisted backend targets

Performance Considerations

Gateway Latency Budget

Every gateway adds latency overhead. The Prismatic Platform allocates the gateway latency budget as follows:

ComponentBudgetActual
Request parsing1ms~0.5ms
Authentication5ms~2ms (ETS lookup)
Rate limiting1ms~0.1ms (ETS counter)
Request validation2ms~1ms
Dispatch1ms~0.5ms
Total overhead10ms~4ms

The BEAM's efficiency for this workload stems from:

  • ETS for O(1) rate limit and auth lookups
  • Plug pipeline compiled to efficient function chains
  • Zero-copy binary handling for JSON payloads
  • Lightweight processes for concurrent request handling

Caching Strategy

The gateway implements a two-level cache:

defmodule PrismaticApi.Cache do
  @moduledoc """
  Gateway response cache with configurable TTL per-endpoint.
  """

  @spec get_or_compute(String.t(), (-> term()), pos_integer()) :: term()
  def get_or_compute(cache_key, compute_fn, ttl_seconds \\ 60) do
    case :ets.lookup(:api_cache, cache_key) do
      [{_, value, expires}] when expires > System.system_time(:second) ->
        value

      _ ->
        value = compute_fn.()
        expires = System.system_time(:second) + ttl_seconds
        :ets.insert(:api_cache, {cache_key, value, expires})
        value
    end
  end
end

Usage in Prismatic Platform

The prismatic_api application (port 4004) serves as the platform's API gateway:

  • Auto-discovery: Scans all Prismatic* modules at boot
  • OpenAPI 3.0: Full specification at /api/openapi
  • SwaggerUI: Interactive documentation at /api/swagger-ui
  • Consistent envelope: All responses wrapped in {data, meta, pagination} structure
  • OSINT integration: All 157 OSINT tools accessible via /api/v1/osint/*
  • DD integration: Case management via /api/v1/dd/*
  • Health checks: System health at /api/v1/health

Key Endpoints

# Discovery
GET  /api/openapi                          # OpenAPI 3.0 specification
GET  /api/swagger-ui                        # Interactive documentation

# OSINT
GET  /api/v1/osint/list_tools             # List all OSINT tools
POST /api/v1/osint/execute_tool           # Execute any OSINT tool
GET  /api/v1/osint/get_run                # Retrieve execution results
POST /api/v1/osint/filter_tools           # Filter tools by category

# DD
GET  /api/v1/dd/cases                     # List DD cases
POST /api/v1/dd/cases                     # Create DD case
GET  /api/v1/dd/entities                  # List entities

# System
GET  /api/v1/health                       # Platform health check
GET  /api/v1/metrics                      # Performance metrics

Best Practices

Design Principles

  1. Single entry point: All external API access flows through the gateway -- no direct backend access
  2. Fail open vs fail closed: Authentication must fail closed (deny by default). Rate limiting can fail open under extreme load
  3. Stateless gateway: Gateway should not store business state -- only transient auth/rate data in ETS
  4. Correlation IDs: Every request gets a unique ID that propagates through all backend calls for tracing
  5. Graceful degradation: If a backend is down, the gateway returns cached data or a meaningful error, not a raw exception

Configuration Guidelines

# Gateway configuration best practices
config :prismatic_api, PrismaticApi.Gateway,
  # Rate limiting
  rate_limit_per_minute: 100,
  rate_limit_burst: 20,

  # Timeouts
  backend_timeout_ms: 15_000,
  gateway_timeout_ms: 30_000,

  # Circuit breaker
  circuit_breaker_threshold: 5,
  circuit_breaker_reset_ms: 30_000,

  # Caching
  default_cache_ttl_seconds: 60,
  max_cache_entries: 10_000,

  # Security
  require_authentication: true,
  cors_origins: ["https://prismatic.example.com"]

Anti-Patterns

Anti-PatternProblemSolution
Business logic in gatewayGateway becomes a bottleneckKeep gateway thin, delegate to backends
No rate limitingDDoS vulnerabilityAlways rate limit external endpoints
Synchronous aggregationSlow response timesUse Task.async_stream for parallel calls
Manual route configurationDrift from backend changesAuto-discovery (Prismatic approach)
Monolithic gatewaySingle point of failurePlug pipeline with independent concerns
No circuit breakerCascading failuresCircuit break each backend independently
  • API -- application programming interface that the gateway exposes
  • REST -- architectural style used by the gateway's external interface
  • Endpoint -- specific URLs that the gateway routes to backends
  • Authentication -- identity verification handled at the gateway
  • Rate Limiting -- traffic management at the gateway level
  • OpenAPI Spec -- API documentation standard used by the gateway
  • Plug -- Phoenix's composable middleware used to build the pipeline
  • Telemetry -- observability instrumentation for gateway metrics
  • Integration -- the broader pattern of connecting systems
  • Circuit Breaker -- resilience pattern for backend failures
  • Middleware -- composable request/response processing layers
  • Pipeline -- sequential processing model underlying the plug chain
  • JSON -- primary data format for gateway communication
  • ETS -- in-memory storage for rate limits and auth cache
  • Reverse Proxy -- simpler network-level routing alternative

See Also


Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL