portfolio Anshul Bisen
ask my work

Why we picked React Server Components over a separate API layer

I tried React Server Components for our internal dashboard and realized I could eliminate the entire API serialization layer for read-heavy pages.

Every architecture diagram I drew for FinanceOps had two boxes at the top: a React frontend and a REST API. The frontend makes HTTP requests, the API returns JSON, and there is a serialization layer in between that converts database rows to API responses and API responses to component props. I never questioned this pattern until I actually tried React Server Components on our internal dashboard and realized half our API endpoints existed solely to shuttle data between the database and the UI.

The shape of the problem before the fix.

In that first stretch at FinanceOps, I was still learning how to wear the Head of Engineering title without hiding behind it. It also builds on what I learned earlier in “Building a payment reconciliation engine that catches every penny.” The only credibility that mattered was whether the decision survived contact with real money, ugly edge cases, and the next person I would eventually hire. That same bias toward strict boundaries later shaped how I approached ftryos and pipeline-sdk: make correctness boring before you make the API clever.

The operational artifact behind the argument.

The Problem With Separate API Layers for Internal Tools

Our internal dashboard had 47 API endpoints. I audited every one of them and sorted them into three categories.

  • Category 1 - Pure data fetching: 31 endpoints that took zero parameters (or just a tenant ID) and returned data directly from a database query. No business logic. No side effects. Just SELECT and serialize.
  • Category 2 - Mutations with business logic: 11 endpoints that validated input, enforced business rules, and modified data. Payment processing, invoice generation, user management. Real API endpoints.
  • Category 3 - Aggregations: 5 endpoints that combined data from multiple tables for dashboard widgets. Revenue summaries, payment volume charts, reconciliation status.

The 31 pure data-fetching endpoints were wasted layers of indirection. They added latency (an extra network hop), maintenance burden (API route, controller, serializer, TypeScript response type), and potential for bugs (incorrect serialization, stale response types). For an internal tool where the frontend and backend ship as a single unit, this overhead bought us nothing.

The Server Component Alternative

React Server Components let the component itself be the data-fetching layer. The component runs on the server, queries the database directly, and sends rendered HTML to the client. No API endpoint, no serialization, no client-side data fetching library.

api/payments/recent.ts
// Before: API endpoint + client component + data fetching
export async function GET(req: Request) {
const tenantId = getTenantId(req);
const payments = await db.query.payments.findMany({
where: eq(payments.tenantId, tenantId),
orderBy: desc(payments.createdAt),
limit: 20,
});
return Response.json(payments);
}
// RecentPayments.tsx (client component)
function RecentPayments() {
const { data } = useSWR('/api/payments/recent');
if (!data) return <Skeleton />;
return <PaymentTable data={data} />;
}
// After: Server Component — no API needed
async function RecentPayments() {
const tenantId = await getCurrentTenant();
const payments = await db.query.payments.findMany({
where: eq(payments.tenantId, tenantId),
orderBy: desc(payments.createdAt),
limit: 20,
});
return <PaymentTable data={payments} />;
}

The server component version is half the code, has zero loading state to manage, and eliminates an entire API route from the codebase. The data flows directly from the database to the rendered HTML without crossing a network boundary or going through a serialization step.

Where Server Components Do Not Replace APIs

Server Components are not a replacement for all API endpoints. They are a replacement for read-only data fetching in pages where the frontend and backend are colocated. The other two categories of endpoints stayed as Route Handlers.

  • Mutations stay as API endpoints because they need to handle form submissions, validate input, and return structured success/error responses. Server Actions help here but the mutation logic still needs a defined contract.
  • External integrations stay as API endpoints because payment processor webhooks, third-party callbacks, and mobile app APIs need stable URLs that exist independently of any React page.
  • Aggregation endpoints moved to server components where they powered dashboard pages, but stayed as API endpoints where they fed data to scheduled email reports or Slack notifications.

The result was a clean split: server components handle read paths for the dashboard UI, and Route Handlers handle mutations and external integrations. The 31 pure data-fetching endpoints were deleted entirely. The codebase is smaller, faster, and easier to maintain.

The Mental Model Shift

The hardest part of adopting Server Components was not the code. It was the mental model. For years I thought in terms of “fetch data, then render.” The client requests data from an API, waits for the response, transforms it into component props, and renders. Server Components invert this: “render is the fetch.” The component itself is the data access layer. There is no separation between fetching and rendering because they happen in the same execution context.

This model does not work for everything. Interactive components that respond to user events still need to be client components. Data that updates in real time still needs a client-side fetching strategy. But for the 70% of our dashboard that is read-only data displayed in tables and charts, Server Components eliminated an entire category of boilerplate and made the code genuinely simpler.

What changed once the system matured.

The builder phase was less glamorous than people imagine. It was mostly a series of stubborn, unfashionable choices that kept future-me out of 2 a.m. incident calls. I still make the same kind of choices inside portfolio, pipeline-sdk, and dotfiles.

Do not build an API endpoint just because that is what the architecture diagram says. If the consumer is a colocated React page that only reads data, a Server Component eliminates the middleman and gives you type safety from database to rendered HTML.

Server components eliminated an entire category of bugs that plagued our previous architecture: stale data, loading state inconsistencies, and cache invalidation failures. The mental model shift was significant — thinking in terms of server and client boundaries rather than API endpoints and frontend state — but the reduction in total code and the improvement in user experience justified the learning curve. For data-heavy applications with small teams, server components are not just a convenience. They are a genuine architectural advantage.