Stripe's public API sits behind a gateway layer that authenticates every request, enforces per-key rate limits, and routes to the internal services that charge cards or issue refunds. A caller never sees that structure, they see one API at one domain, and Stripe can move a route to a different service without any client noticing. This is the work an API gateway does, giving clients one stable front door while the services behind it stay free to change.
The pattern is useful, and it is also easy to reach for too early. A gateway is another process to run, another hop on every request, and a place where routing and auth logic collects. For a system with a couple of services it often costs more than it returns, so the useful skill is knowing which gateway responsibilities you need and where they should live.
Clients send requests to a single API gateway, which authenticates, rate-limits, and routes each request to one of several backend services.
The word "gateway" covers a bundle of responsibilities that tend to travel together because they all belong at the boundary of the system, before a request reaches any single service. Pulling them apart makes it easier to see which ones you need.
/orders reaches the orders service and /users reaches the users service. Clients see one host, and you can move a route to a different service without touching callers.Routing and auth are the two most teams reach for. Aggregation is the one to be careful with, because putting slow, multi-service business logic on the request path is how a thin gateway turns into a fat one that becomes hard to keep available.
Authentication is the responsibility that most clearly benefits from living at the edge. If every service validated tokens on its own, you would duplicate that logic across the codebase and have to update all of it whenever the scheme changed. Doing it once at the boundary keeps token handling in one place, and downstream services receive an identity they can trust rather than a raw credential they have to verify.
The shape is: the gateway checks the credential, resolves it to a caller, and passes that caller inward. A service reads the caller from the request context and makes its own authorization decision, such as whether this user may cancel that order, without going back to the token. Encore models this directly with an auth handler that runs before your endpoints and populates the caller data your handlers read:
import { Header, Gateway } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
import { getAuthData } from "~encore/auth";
interface AuthParams {
authorization: Header<"Authorization">;
}
interface AuthData {
userID: string;
}
// Runs at the edge, before any endpoint that requires auth.
export const auth = authHandler<AuthParams, AuthData>(async (params) => {
const userID = await verifyToken(params.authorization);
return { userID };
});
export const gateway = new Gateway({ authHandler: auth });
Inside an endpoint marked auth: true, the verified caller is available through getAuthData(), so the endpoint decides what this user is allowed to do without re-checking the token.
The strongest argument against a separate gateway is that a small system rarely uses one enough to justify it. With one or two services, routing is trivial, there is no cross-service aggregation to do, and auth can live in a shared middleware. Adding a gateway there gives you an extra deployable, an extra network hop on every request, and a single point of failure to make redundant, in exchange for indirection you do not need yet.
A dedicated gateway starts to pay off when one or more of these is true:
Below that bar, the same responsibilities are better handled by your framework or a shared library than by a standalone component. Many modern backend frameworks fold routing and edge auth into the application itself, which covers the common cases without a separate process to run. The service-to-service communication patterns cover how services then talk to each other behind that boundary.
A decision flow: default to no dedicated gateway; add one only when you have public multi-service APIs, external consumers, or cross-cutting edge needs.
A gateway sees every request, so its failure modes matter more than any single service's. Two rules keep it from becoming the weak point. Keep its logic thin, so it routes, authenticates, and rate-limits but does not run slow business logic that ties up a request while it waits on a database. And run it as several instances behind a load balancer with health checks, so no single instance is on the critical path. The Backend for Frontend variant needs extra care here, because a BFF that aggregates several service calls per request is doing real work on the hot path, and you want timeouts and fallbacks so one slow downstream service does not stall the whole response.
| Concern | Thin gateway | Fat gateway |
|---|---|---|
| Work per request | Route, auth, rate-limit | Also aggregates and transforms data |
| Latency added | Small, predictable | Grows with downstream calls |
| Availability | Easy to keep redundant | Harder, more failure paths |
| Best fit | Most systems | BFFs and deliberate aggregation layers |
A large part of the case for a dedicated gateway is centralizing routing and edge auth across services, and Encore handles both inside the application. You define endpoints as typed functions and mark which require authentication, and Encore generates a gateway that terminates TLS, runs your auth handler at the edge, and routes each request to the right service. There is no separate gateway process to configure and keep running, because the routing and auth live in the same codebase as the services.
Because services call each other with type-checked function calls through the ~encore/clients import rather than over ad hoc HTTP, the internal boundaries a gateway usually hides are already type-safe and traced. For a system that would otherwise stand up a gateway mainly to centralize auth and routing, that need is met by the framework. When you do want a standalone edge component in front of everything, encore build docker produces a standard container image, so putting a dedicated gateway or load balancer ahead of it stays a normal deployment choice. This tends to track the monolith versus microservices decision: the fewer moving pieces you run, the less gateway machinery you need to bolt on.
An API gateway is a single entry point that sits in front of your services and handles work that would otherwise be repeated in each one. It routes each request to the right service, terminates TLS, authenticates callers, applies rate limits, and can combine responses from several services into one. Clients talk to the gateway instead of to individual services, so internal service boundaries stay hidden and can change without breaking callers.
Not always. A gateway earns its place once you have several public-facing services, external API consumers, or cross-cutting needs like centralized auth and rate limiting. For a small system with one or two services, a dedicated gateway is usually an extra hop and an extra thing to run for little gain. Many backend frameworks already handle routing and auth at the edge, which covers the common cases without a separate component.
A load balancer spreads traffic across identical copies of a service and works at the connection or HTTP level without understanding your API. An API gateway works at the application level: it reads the request, decides which service should handle it, checks auth, applies rate limits, and can reshape the response. You often run both, with the load balancer in front distributing traffic and the gateway routing by path or method.
Backend for Frontend is a gateway variant where each client type gets its own tailored entry point. A web app, a mobile app, and a partner API each talk to a BFF shaped for their needs, so the mobile BFF can return a smaller payload and merge three service calls into one response. It keeps client-specific shaping out of the core services, at the cost of running and maintaining one gateway per client.
Authenticate at the edge, then pass a verified identity inward. The gateway or API layer validates the token or session once, rejects anonymous traffic early, and forwards the caller's identity to downstream services so each one can make authorization decisions without revalidating the credential. This keeps token handling in one place and means internal services trust a claim they receive rather than parsing raw credentials repeatedly.
Yes, since every request passes through it, so a gateway needs redundancy, health checks, and enough capacity to absorb traffic spikes. Run it as multiple instances behind a load balancer, keep its logic thin, and avoid putting slow business logic in it. A gateway that only routes, authenticates, and rate-limits is easier to keep highly available than one that also aggregates data from many services on the hot path.