An API gateway sits between your frontend and backend services, providing a unified interface. Here is how to build one.
Gateway Core
// lib/gateway.ts
type Middleware = (
ctx: GatewayContext,
next: () => Promise<void>
) => Promise<void>;
interface GatewayContext {
request: Request;
params: Record<string, string>;
headers: Headers;
response: Response | null;
state: Record<string, unknown>;
}
export class ApiGateway {
private middlewares: Middleware[] = [];
private routes = new Map<string, { method: string; pattern: RegExp; paramNames: string[]; handler: Middleware }>();
use(middleware: Middleware) {
this.middlewares.push(middleware);
return this;
}
route(method: string, path: string, handler: Middleware) {
const paramNames: string[] = [];
const regexStr = path.replace(/:(\w+)/g, (_, name) => {
paramNames.push(name);
return "([^/]+)";
});
this.routes.set(`${method}:${path}`, {
method: method.toUpperCase(),
pattern: new RegExp(`^${regexStr}$`),
paramNames,
handler,
});
return this;
}
async handle(request: Request, pathname: string): Promise<Response> {
const ctx: GatewayContext = {
request,
params: {},
headers: new Headers(request.headers),
response: null,
state: {},
};
// Match route
let routeHandler: Middleware | null = null;
for (const route of this.routes.values()) {
if (route.method !== request.method) continue;
const match = pathname.match(route.pattern);
if (match) {
route.paramNames.forEach((name, i) => {
ctx.params[name] = match[i + 1];
});
routeHandler = route.handler;
break;
}
}
if (!routeHandler) {
return new Response(JSON.stringify({ error: "Not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
// Build middleware chain
const allMiddlewares = [...this.middlewares, routeHandler];
let index = 0;
const next = async () => {
if (index < allMiddlewares.length) {
const mw = allMiddlewares[index++];
await mw(ctx, next);
}
};
try {
await next();
return ctx.response ?? new Response(null, { status: 204 });
} catch (error) {
console.error("Gateway error:", error);
return new Response(
JSON.stringify({ error: "Internal gateway error" }),
{ status: 502, headers: { "Content-Type": "application/json" } }
);
}
}
}
Middleware: Authentication
// lib/gateway/auth.ts
import type { Middleware } from "../gateway";
export function authMiddleware(
verifyToken: (token: string) => Promise<{ userId: string; role: string } | null>
): Middleware {
return async (ctx, next) => {
const authHeader = ctx.request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
ctx.response = new Response(
JSON.stringify({ error: "Missing authorization header" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
return;
}
const token = authHeader.slice(7);
const user = await verifyToken(token);
if (!user) {
ctx.response = new Response(
JSON.stringify({ error: "Invalid or expired token" }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
return;
}
ctx.state.userId = user.userId;
ctx.state.userRole = user.role;
ctx.headers.set("x-user-id", user.userId);
ctx.headers.set("x-user-role", user.role);
await next();
};
}
Middleware: Rate Limiting
// lib/gateway/rate-limit.ts
const requestCounts = new Map<string, { count: number; resetAt: number }>();
export function rateLimitMiddleware(
limit: number = 100,
windowMs: number = 60_000
): Middleware {
return async (ctx, next) => {
const ip = ctx.request.headers.get("x-forwarded-for") ?? "unknown";
const now = Date.now();
const entry = requestCounts.get(ip);
if (!entry || now > entry.resetAt) {
requestCounts.set(ip, { count: 1, resetAt: now + windowMs });
} else {
entry.count++;
if (entry.count > limit) {
ctx.response = new Response(
JSON.stringify({ error: "Too many requests" }),
{
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": String(Math.ceil((entry.resetAt - now) / 1000)),
},
}
);
return;
}
}
await next();
};
}
Middleware: Service Proxy
// lib/gateway/proxy.ts
export function proxyTo(baseUrl: string): Middleware {
return async (ctx, _next) => {
const url = new URL(ctx.request.url);
const targetUrl = `${baseUrl}${url.pathname}${url.search}`;
const proxyHeaders = new Headers(ctx.headers);
proxyHeaders.delete("host");
const response = await fetch(targetUrl, {
method: ctx.request.method,
headers: proxyHeaders,
body: ctx.request.method !== "GET" ? ctx.request.body : undefined,
// @ts-expect-error duplex needed for streaming
duplex: "half",
});
ctx.response = new Response(response.body, {
status: response.status,
headers: response.headers,
});
};
}
Middleware: Response Aggregation
// lib/gateway/aggregate.ts
interface ServiceCall {
key: string;
url: string;
headers?: Record<string, string>;
}
export function aggregateMiddleware(services: (ctx: GatewayContext) => ServiceCall[]): Middleware {
return async (ctx, _next) => {
const calls = services(ctx);
const results = await Promise.allSettled(
calls.map(async (call) => {
const res = await fetch(call.url, {
headers: { "Content-Type": "application/json", ...call.headers },
});
if (!res.ok) throw new Error(`${call.key}: ${res.status}`);
return { key: call.key, data: await res.json() };
})
);
const aggregated: Record<string, unknown> = {};
const errors: string[] = [];
results.forEach((result, i) => {
if (result.status === "fulfilled") {
aggregated[result.value.key] = result.value.data;
} else {
errors.push(calls[i].key);
aggregated[calls[i].key] = null;
}
});
ctx.response = new Response(
JSON.stringify({ data: aggregated, errors: errors.length > 0 ? errors : undefined }),
{
status: errors.length === calls.length ? 502 : 200,
headers: { "Content-Type": "application/json" },
}
);
};
}
Setting Up Routes
// lib/gateway/index.ts
import { ApiGateway } from "../gateway";
import { authMiddleware } from "./auth";
import { rateLimitMiddleware } from "./rate-limit";
import { proxyTo } from "./proxy";
import { aggregateMiddleware } from "./aggregate";
export const gateway = new ApiGateway();
// Global middleware
gateway.use(rateLimitMiddleware(100, 60_000));
// Public routes — proxy to services
gateway.route("GET", "/products", proxyTo(process.env.PRODUCTS_SERVICE_URL!));
gateway.route("GET", "/products/:id", proxyTo(process.env.PRODUCTS_SERVICE_URL!));
// Protected routes
gateway.route("GET", "/dashboard", async (ctx, next) => {
await authMiddleware(verifyToken)(ctx, async () => {
await aggregateMiddleware((ctx) => [
{ key: "user", url: `${process.env.USERS_SERVICE_URL}/users/${ctx.state.userId}` },
{ key: "orders", url: `${process.env.ORDERS_SERVICE_URL}/orders?userId=${ctx.state.userId}` },
{ key: "notifications", url: `${process.env.NOTIFICATIONS_URL}/notifications?userId=${ctx.state.userId}` },
])(ctx, next);
});
});
async function verifyToken(token: string) {
// Verify JWT or call auth service
return { userId: "user-1", role: "member" };
}
Next.js Route Handler
// app/api/gateway/[...path]/route.ts
import { NextRequest } from "next/server";
import { gateway } from "@/lib/gateway";
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
return gateway.handle(request, `/${path.join("/")}`);
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
return gateway.handle(request, `/${path.join("/")}`);
}
Need Microservice Architecture?
We design and build API gateways, service meshes, and microservice architectures. Contact us to discuss your backend needs.