React + .NET Web API: A Practical Architecture
How I structure a React front end against a .NET Web API — project layout, the API contract, authentication and the boundaries that keep both sides independent.
Starter draft — adapt the specifics to the conventions you prefer before publishing.
React and .NET is a pairing I have used on several enterprise applications. Neither side is interesting on its own; what determines whether the project stays maintainable is the boundary between them.
Keep the boundary explicit#
The single decision that matters most: the API is the contract, and neither side reaches across it.
- The front end does not know table names, ORM entities, or how a value is computed.
- The API does not know about component structure, routing, or how a screen is laid out.
When that boundary blurs — an endpoint shaped around one screen, a response that mirrors a database table — every UI change becomes a backend change.
Project layout#
A structure that has worked well for medium-sized applications:
src/
Api/ # ASP.NET Core Web API — controllers, DI, middleware
Application/ # Use cases, DTOs, validation, business rules
Domain/ # Entities and domain logic, no infrastructure
Infrastructure/ # EF Core, external services, implementations
Web/ # React application
src/
api/ # Typed client; one module per resource
components/ # Reusable, presentational
features/ # Feature folders: screens + local state
hooks/
lib/
types/ # Shared types mirroring API DTOs
The important property is not the exact folder names — it is that business rules live somewhere that does not depend on ASP.NET or on Entity Framework, so they can be tested without either.
Design the API around resources, not screens#
Endpoints shaped around a particular page age badly, because pages change more often than the domain does.
GET /api/v1/orders?status=placed&page=1&pageSize=25
GET /api/v1/orders/{id}
POST /api/v1/orders
PATCH /api/v1/orders/{id}/status
Conventions worth fixing early, because they are painful to change later:
- Versioning in the path.
/api/v1/costs nothing now and buys a migration path later. - One envelope for collections.
{ data, meta: { page, pageSize, total } }everywhere. - One error shape. RFC 7807 problem details, or your own equivalent — but the same one for every endpoint.
- Explicit status codes. 400 for validation, 401 versus 403 used correctly, 409 for conflicts, 422 where it applies.
{ "type": "https://example.com/errors/validation", "title": "Validation failed", "status": 400, "errors": { "customerId": ["Customer does not exist."] } }
If the front end can handle errors with one function, the contract is consistent enough.
A typed client on the front end#
Rather than scattering fetch calls through components, keep one module per resource:
// api/orders.ts import { http } from './http'; import type { Order, OrderStatus, Paginated } from '../types'; export const listOrders = (params: { status?: OrderStatus; page?: number }) => http.get<Paginated<Order>>('/orders', { params }); export const getOrder = (id: string) => http.get<Order>(`/orders/${id}`); export const updateOrderStatus = (id: string, status: OrderStatus) => http.patch<Order>(`/orders/${id}/status`, { status });
Components then depend on functions with meaningful names and types, not on URLs. When an endpoint changes, one file changes.
Where the DTOs are stable, generating the TypeScript types from the OpenAPI document removes a whole category of drift between the two codebases.
Authentication#
For enterprise applications this usually means an identity provider rather than rolling your own: the SPA acquires a token, the API validates it, and authorisation decisions are made server-side against the token's claims.
Two rules that are not worth bending:
- The UI hides actions; the API enforces them. Hiding a button is a usability improvement, not a security control.
- Tokens are handled deliberately. Where they are stored, when they refresh, and what happens to an in-flight request when one expires.
What belongs where#
| Concern | Front end | API |
|---|---|---|
| Input formatting and immediate feedback | Yes | — |
| Validation that must hold | Mirror for UX | Authoritative |
| Authorisation | Render from server's answer | Authoritative |
| Business calculations | No | Yes |
| Pagination, filtering, sorting | Requests it | Performs it |
Anything marked authoritative on the API side may be duplicated on the front end for user experience, but never moved there.
Things I would decide early#
- Response envelope and error shape
- Pagination style and its defaults
- Date and time handling — store and transmit UTC, format at the edge
- Correlation identifiers flowing from the browser through to the logs
- Where validation messages come from, so the UI is not translating error codes by hand
None of these are difficult decisions. They are just expensive to change once fifty endpoints exist.