Blog/Tutorials & Step-by-Step/How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
POST
March 21, 2026
LAST UPDATEDMarch 21, 2026

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

Tags

Next.jsNode.jsBFFArchitectureAPI Design
How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
6 min read

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

TL;DR

In this tutorial, you will build a Backend-for-Frontend using Next.js and Node.js that aggregates upstream APIs, centralizes authentication, shapes responses for the UI, and reduces frontend complexity. A BFF is most valuable when your frontend has orchestration needs that backend services should not own directly.

Prerequisites

Before you start, you should be comfortable with:

  • Next.js App Router fundamentals
  • route handlers in Next.js
  • basic Node.js API design
  • JSON Web Tokens or session-based authentication
  • calling REST or GraphQL APIs from server-side code

You do not need a microservices platform to benefit from this pattern. Even a mid-sized product with a few backend services can justify a BFF if the frontend is doing too much coordination on its own.

Step 1: Understand What a BFF Should Own

A Backend-for-Frontend is not just another API. It is a frontend-oriented orchestration layer.

In practice, that means the BFF usually owns:

  • aggregating data from multiple upstream services
  • translating service contracts into UI-friendly payloads
  • handling auth and token forwarding
  • applying frontend-specific caching and fallback logic
  • isolating the UI from backend churn

It usually should not own:

  • core business rules that belong in domain services
  • long-term system-of-record logic
  • data models that need to be shared uniformly across channels

The simplest mental model is this: domain services own business capabilities, and the BFF owns delivery of those capabilities to one frontend surface.

Step 2: Create a Simple BFF Route

In a Next.js App Router application, route handlers are a practical place to start.

tsx
// app/api/dashboard/route.ts
import { NextResponse } from "next/server";
 
export async function GET() {
  const [profileRes, notificationsRes] = await Promise.all([
    fetch(`${process.env.USER_SERVICE_URL}/me`, {
      headers: { Authorization: `Bearer ${process.env.INTERNAL_TOKEN}` },
      cache: "no-store",
    }),
    fetch(`${process.env.NOTIFICATION_SERVICE_URL}/notifications`, {
      headers: { Authorization: `Bearer ${process.env.INTERNAL_TOKEN}` },
      cache: "no-store",
    }),
  ]);
 
  if (!profileRes.ok || !notificationsRes.ok) {
    return NextResponse.json(
      { message: "Failed to load dashboard data" },
      { status: 502 }
    );
  }
 
  const [profile, notifications] = await Promise.all([
    profileRes.json(),
    notificationsRes.json(),
  ]);
 
  return NextResponse.json({
    user: {
      id: profile.id,
      name: profile.name,
      role: profile.role,
    },
    unreadNotifications: notifications.items.filter((item: any) => !item.read),
  });
}

This route is not just proxying. It is:

  • calling multiple upstream systems
  • shaping the response for the dashboard
  • hiding internal service complexity from the frontend

That is the core value of the BFF pattern.

Step 3: Move Upstream Logic into a Service Layer

If you leave all orchestration inside route handlers, the BFF becomes messy fast. A cleaner pattern is to keep handlers thin and move integration logic into application services.

ts
// lib/bff/dashboard-service.ts
type DashboardData = {
  user: {
    id: string;
    name: string;
    role: string;
  };
  unreadNotifications: Array<{
    id: string;
    title: string;
  }>;
};
 
export async function getDashboardData(token: string): Promise<DashboardData> {
  const [profileRes, notificationsRes] = await Promise.all([
    fetch(`${process.env.USER_SERVICE_URL}/me`, {
      headers: { Authorization: `Bearer ${token}` },
      cache: "no-store",
    }),
    fetch(`${process.env.NOTIFICATION_SERVICE_URL}/notifications`, {
      headers: { Authorization: `Bearer ${token}` },
      cache: "no-store",
    }),
  ]);
 
  if (!profileRes.ok) {
    throw new Error("Failed to fetch user profile");
  }
 
  if (!notificationsRes.ok) {
    throw new Error("Failed to fetch notifications");
  }
 
  const [profile, notifications] = await Promise.all([
    profileRes.json(),
    notificationsRes.json(),
  ]);
 
  return {
    user: {
      id: profile.id,
      name: profile.name,
      role: profile.role,
    },
    unreadNotifications: notifications.items
      .filter((item: any) => !item.read)
      .map((item: any) => ({
        id: item.id,
        title: item.title,
      })),
  };
}

Then the route handler becomes much easier to test and reason about.

ts
// app/api/dashboard/route.ts
import { NextResponse } from "next/server";
import { getDashboardData } from "@/lib/bff/dashboard-service";
 
export async function GET() {
  try {
    const token = process.env.INTERNAL_TOKEN!;
    const data = await getDashboardData(token);
    return NextResponse.json(data);
  } catch {
    return NextResponse.json(
      { message: "Unable to load dashboard data" },
      { status: 502 }
    );
  }
}

Step 4: Handle Authentication at the BFF Boundary

One of the strongest reasons to use a BFF is auth centralization. Instead of making the frontend manage multiple downstream tokens, the BFF can validate the user session once and call upstream services on the user's behalf.

A common pattern looks like this:

  1. the browser sends a session cookie to the BFF
  2. the BFF validates the session
  3. the BFF derives or exchanges service credentials
  4. the BFF calls upstream services
  5. the frontend gets only the shaped data it needs

That reduces token sprawl in the client and gives you a clean place to enforce access rules.

Step 5: Shape Responses for the UI, Not for Backend Purity

A mistake many teams make is returning backend-shaped payloads directly through the BFF. That defeats the point.

The BFF should optimize for:

  • fewer round trips
  • stable frontend contracts
  • component-friendly payloads
  • explicit fallback behavior

For example, if your UI needs a single accountSummary object, the BFF can combine billing, user, and usage data into one response. That is often better than making the frontend assemble it itself.

Step 6: Add Caching Only Where It Helps

Because the BFF sits close to the UI, it is a good place for frontend-oriented caching decisions. But caching needs discipline.

Good candidates:

  • semi-static dashboard summaries
  • content listings
  • read-heavy aggregate endpoints

Bad candidates:

  • highly user-specific correctness-sensitive data
  • rapidly changing operational workflows
  • permission-sensitive data with unclear invalidation rules

If you cache through the BFF, define freshness and invalidation rules explicitly. Otherwise, the BFF becomes a source of stale and confusing behavior.

Step 7: Handle Partial Failure Intentionally

A BFF often depends on multiple upstream systems. That means partial failure is normal, not exceptional.

A strong BFF design answers:

  • what if one service is slow?
  • what if one service is down?
  • what if one field is optional and another is critical?

In many products, the best response is not full failure. It is degraded success.

ts
return NextResponse.json({
  user,
  unreadNotifications: notifications ?? [],
  warnings: notifications ? [] : ["Notifications are temporarily unavailable"],
});

That gives the UI a graceful fallback path instead of a binary pass/fail outcome.

The Complete Example Structure

For a real BFF in Next.js and Node.js, a practical file layout looks like this:

text
app/
  api/
    dashboard/
      route.ts
lib/
  bff/
    dashboard-service.ts
    auth.ts
    upstream-clients/
      user-service.ts
      notification-service.ts
  validation/
    dashboard-schema.ts

That structure keeps concerns separate:

  • route handler for transport
  • service layer for orchestration
  • upstream clients for external contracts
  • validation layer for safety and consistency

Common Pitfalls

Turning the BFF into a Dumping Ground

If every frontend workaround, auth rule, and temporary transformation lands in the BFF without structure, the layer becomes harder to maintain than the frontend it was meant to simplify.

Owning Business Logic That Belongs Elsewhere

The BFF should coordinate and shape data, not become the permanent home of domain rules that should live in shared services.

Mirroring Upstream APIs Too Closely

If the BFF is just a thin proxy, you gain little. The value comes from translation, aggregation, and boundary control.

Skipping Contract Validation

If upstream services change shape and the BFF does not validate responses, the frontend ends up breaking in confusing ways. Validate at the boundary whenever possible.

When to Use a BFF and When Not To

Use a BFF when:

  • the frontend consumes multiple services
  • auth and orchestration are leaking into the UI
  • different frontend surfaces need different contracts
  • backend service churn is disrupting delivery

Do not reach for a BFF automatically when:

  • the app is small and stable
  • one backend already provides the exact contract needed
  • a new layer would add more operational cost than value

Next Steps

Once the basic BFF is working, the next improvements usually are:

  • schema validation for upstream responses
  • traceability and observability
  • better timeout and retry handling
  • cache invalidation strategy
  • contract tests against upstream services

That is when a simple BFF starts becoming a dependable production boundary instead of just an API convenience layer.

FAQ

What is a Backend-for-Frontend architecture?

A Backend-for-Frontend is a server layer designed specifically for a frontend application or channel. It aggregates data, shapes responses, enforces auth, and hides backend complexity from the UI.

Why use Next.js with a BFF pattern?

Next.js works well with BFF architecture because route handlers, server components, and server actions can coordinate API calls and shape data close to the UI layer.

When should you not use a BFF?

A BFF may be unnecessary for very small applications with simple backend APIs and limited orchestration needs. It adds a layer, so it should solve a real frontend coordination problem.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.

OpenTelemetry for Next.js and Node.js
Mar 21, 20265 min read
OpenTelemetry
Next.js
Node.js

OpenTelemetry for Next.js and Node.js

A practical implementation guide for adding OpenTelemetry to Next.js and Node.js apps, including traces, request flow visibility, and production diagnostics.