Skip to main content
Back to Blog
Tutorials
3 min read
January 5, 2025

How to Build an API Gateway Pattern in Next.js

Implement an API gateway pattern in Next.js to aggregate multiple backend services, handle authentication, rate limiting, and response transformation.

Ryel Banfield

Founder & Lead Developer

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.

API gatewaymicroservicesproxyNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles