The Backend a React Developer Actually Needs to Understand

"That's a backend concern" is one of the most expensive sentences a frontend developer can say.
I understand the instinct. There's a clean line in most people's heads: the backend team owns the server, we own the browser, and the API is the wall between us. Someone hands you an endpoint, you fetch from it, you render. Not your problem what happens on the other side.
Except the shape of that wall decides how much of your job is real work versus glue code. I've spent weeks of my life writing frontend that existed only to paper over an awkward API — flattening nested responses, stitching three calls into one screen, re-deriving fields the server could have just sent. That's not frontend work. That's tax, paid in your codebase, because of a decision made on the other side of a wall you decided wasn't yours.
So this article isn't about writing servers. It's about the part of the backend that's genuinely yours as a React developer: the contract. How you talk to the server — REST, tRPC, or a BFF — shapes your frontend more than most of us are willing to admit.
The Contract Is the Product
Here's the reframe that changed how I work: from the frontend, the backend is its API. You don't experience the database, the services, the queues. You experience the contract — the shape of the requests and responses. And a good contract makes the frontend small, while a bad one makes it a translation layer.
That's the lens for everything below. Not "which technology is trendy," but "which contract leaves the least incidental work in my React code." Because every gap between what the server sends and what the UI needs gets filled by you, in components that should be about rendering and instead are about reshaping data.
REST: The Default, and Its Real Cost
REST is the lingua franca. Everything speaks it, every tool supports it, and for a lot of applications it's completely fine. When someone hands me a well-designed REST API — sensible resources, predictable shapes, the fields I need where I expect them — I have no complaints. It pairs perfectly with TanStack Query, which was practically built for the REST request/response model.
The costs of REST are real but they're not "REST is bad." They're two specific frictions.
The first is over- and under-fetching. An endpoint returns a fixed shape. Sometimes that's more than your screen needs, so you ship kilobytes you throw away. More often it's less than you need, so you make a second call, then a third, and now one screen is a waterfall of requests you have to orchestrate and cache.
The second, and the one that actually costs me time, is types. A REST response is JSON — untyped by nature. On the client it arrives as any unless you do something about it. Which means the boundary between "typed React app" and "untyped network" runs right through your data layer, and you spend effort re-establishing types the backend already knew.
// The gap, made concrete: const res = await fetch('/api/policies/42'); const policy = await res.json(); // any — the types stop here // so you rebuild what the server already knew: const policy = policySchema.parse(await res.json()); // Zod, by hand
That policySchema is you, manually, re-declaring a shape the backend already has in its own code. It works — I do it, and validating at the boundary with Zod is genuinely good practice. But notice what it is: the same truth, written twice, on two sides of the wall, kept in sync by discipline. Hold that thought, because the next option deletes it.
tRPC: When You Own Both Sides
If your backend is TypeScript, and especially if it's the same repo or monorepo as your frontend, tRPC changes the entire equation. It gives you end-to-end type safety with no schema, no code generation, no manual sync.
You define a procedure on the server, and the client just knows its types — inputs, outputs, everything — inferred straight across the boundary.
// server: the procedure const policyRouter = router({ byId: publicProcedure .input(z.object({ id: z.number() })) .query(({ input }) => getPolicy(input.id)), // returns Policy }); // client: fully typed, no codegen, no fetch wiring const policy = await trpc.policy.byId.query({ id: 42 }); // ^? Policy — inferred end to end
Look at what disappeared. No policySchema written twice. No any at the network boundary. Rename a field on the server and your React code fails to compile before you run it — the contract enforces itself. This is the same instinct the whole series keeps returning to: one source of truth, let the compiler carry it. tRPC just extends that line across the network, which is the one place REST couldn't.
I'll be honest about the constraints, because they're firm. tRPC needs TypeScript on both ends, and it works best when the same team owns both. It's not for a public API consumed by clients you don't control, and it's not what you reach for when a separate backend team ships you REST or GraphQL. But for the shape this series keeps building — a TypeScript frontend and backend under one roof — it removes an entire category of busywork. When I control both sides, it's my default, for the same reason I pick every other tool in this series: it makes the frontend smaller.
GraphQL: The Over/Under-Fetching Answer
GraphQL deserves a fair mention, because it targets REST's first friction directly. The client asks for exactly the fields it needs — no more, no less — in one request, even across what would have been several REST resources. For a complex UI assembling data from many places, that's a real answer to the waterfall problem, and the typed schema helps on the client too.
I reach for it less than I used to, and it's a cost-benefit call, not a dismissal. GraphQL brings its own server, its own caching model, its own operational weight. On a large product with many clients and genuinely graph-shaped data, that weight pays for itself. On the mid-size TypeScript app this series targets, tRPC gets me most of the type-safety win for a fraction of the machinery. Right tool, right scale.
The BFF: A Backend That Works for the Frontend
There's one pattern worth knowing by name, because at some point you'll need it and it helps to know it has one: the Backend for Frontend.
The situation is familiar. You don't control the backend. It's a separate team, or several, exposing APIs designed for their convenience, not your screen's. Your dashboard needs data from three services in shapes that don't match what any of them return. The naive fix is to do all that stitching in React — three calls, merge, reshape, re-derive — and now your components are a data-integration layer wearing a UI costume.
A BFF is a thin server layer that belongs to the frontend and sits between your React app and those messy upstream services. Its whole job is to talk to them and hand your UI exactly the shape it wants, in one call.
// BFF endpoint: one call for the frontend, // three upstream calls hidden behind it async function getDashboard(userId: string) { const [profile, policies, claims] = await Promise.all([ profileService.get(userId), policyService.listFor(userId), claimService.recentFor(userId), ]); return shapeForDashboard(profile, policies, claims); // exactly what the UI renders }
The reason I like the BFF isn't just tidiness. It moves the stitching to where it belongs. Reshaping data, aggregating services, adapting an awkward upstream contract — that's backend work, and doing it in a Node BFF is cheaper, more testable, and more cacheable than doing it in React components. Your frontend goes back to rendering. And if you're already in a TanStack Start or Next.js world, you have a natural place to put a BFF without standing up a whole separate service.
The through-line to the whole article is right here: whenever you find your React code reshaping data, ask whether that reshaping belongs on the server instead. Usually it does.
How I Actually Decide
Strip it to the decision:
- A separate team ships you REST or GraphQL → consume it with TanStack Query, validate at the boundary with Zod, and consider a BFF the moment your components start stitching.
- You own a TypeScript backend, same repo or org → tRPC. End-to-end types, no manual sync, the frontend stays small.
- Complex graph-shaped data, many clients, real scale → GraphQL earns its weight.
- You don't own the upstream and the shapes fight your UI → a BFF, so the reshaping lives on the server where it's cheap.
None of this requires you to write a real backend. It requires you to have an opinion about the contract, and to push on it when it's making your frontend do work it shouldn't. That's the part that was always yours.
I used to think staying out of backend decisions kept my work clean. It did the opposite — it let other people's convenience become my complexity. The most leverage I've found as a frontend developer isn't a faster component or a cleverer hook. It's a better contract, argued for early, that makes half the frontend code simply unnecessary. You don't have to build the server. You do have to care what it sends you.
The next article goes one layer deeper into that same server: SSR and React with TanStack Start — where "frontend" and "backend" stop being two places and start being one framework, and what that does to everything we've built so far.
If you're staring at a React codebase that's mostly reshaping awkward API responses, that's usually a contract problem wearing a frontend costume — tell me what the API looks like and I'll tell you where I'd put the seam.