Frontend Developers Need to Understand API Design
Frontend developers are the primary consumers of APIs, yet API design is rarely part of frontend education. We accept whatever the backend gives us, massage it in the UI, and move on. But understanding API design principles makes you dramatically more effective — you can push back on bad contracts, propose better structures, and build frontend architectures that don't require constant data transformation.
Whether you're working with REST, GraphQL, or tRPC, these principles apply. Good API design is language-agnostic.
#1 Resource Design and Naming Conventions
The most common API design mistake is treating endpoints like function calls (POST /getUsers) instead of resources. REST is built on the concept of resources identified by URLs, with HTTP methods expressing the action.
Design URLs as Resources, Not Actions
# ❌ RPC-style (action in the URL)
POST /getUsers
POST /createUser
POST /deleteUser?id=123
POST /updateUserEmail
# ✅ REST-style (resource + HTTP method)
GET /api/users # List users
POST /api/users # Create user
GET /api/users/123 # Get single user
PATCH /api/users/123 # Update user
DELETE /api/users/123 # Delete user
# Nested resources for relationships
GET /api/users/123/orders # User's orders
GET /api/users/123/orders/456 # Specific order
POST /api/users/123/orders # Create order for user- Use nouns (users, orders) not verbs (getUsers, createOrder)
- Use plural nouns consistently: /users not /user
- Nest resources for clear relationships: /users/123/orders
- Keep nesting shallow — max 2 levels deep
- Use query params for filtering, sorting, pagination: /users?role=admin&sort=name&page=2
- Predictable URL structure — developers can guess endpoints
- HTTP methods carry semantic meaning (GET is cacheable, idempotent)
- Consistent naming reduces documentation burden
#2 Error Handling: The Most Neglected API Feature
Most APIs return 500 with a generic message when something goes wrong. Frontend developers need structured, consistent error responses to build reliable error UIs — validation messages, field-level errors, and actionable descriptions.
Structured Error Responses
// Consistent error response shape
interface ApiError {
status: number;
code: string; // Machine-readable error code
message: string; // Human-readable message
details?: FieldError[]; // Field-level validation errors
}
interface FieldError {
field: string;
message: string;
code: string;
}
// Example: validation error response
export async function POST(req: Request) {
const body = await req.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return Response.json({
status: 422,
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
details: result.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
})),
}, { status: 422 });
}
// ... create user
}On the frontend, this structure enables precise error rendering:
async function handleSubmit(data: FormData) {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data),
});
if (!res.ok) {
const error: ApiError = await res.json();
if (error.code === 'VALIDATION_ERROR' && error.details) {
// Map field errors to form state
error.details.forEach(({ field, message }) => {
setFieldError(field, message);
});
} else {
// Show generic error toast
toast.error(error.message);
}
return;
}
// Success path
}- Field-level validation errors render inline, not as generic toasts
- Machine-readable error codes enable programmatic error handling
- Consistent shape means one error handler works across all endpoints
#3 Pagination, Filtering, and the N+1 Problem
Every list endpoint needs pagination — but the implementation matters. Offset-based pagination breaks with concurrent writes. Response shapes vary wildly between APIs. And front-end developers often make N+1 requests to fetch related data.
Cursor-Based Pagination
// Cursor-based pagination: stable, performant, consistent
interface PaginatedResponse<T> {
data: T[];
pagination: {
cursor: string | null; // null means no more pages
hasMore: boolean;
total?: number; // Optional: expensive for large datasets
};
}
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const cursor = searchParams.get('cursor');
const limit = Math.min(Number(searchParams.get('limit') ?? 20), 100);
const role = searchParams.get('role');
const users = await db.user.findMany({
where: role ? { role } : undefined,
take: limit + 1, // Fetch one extra to determine hasMore
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return Response.json({
data,
pagination: {
cursor: hasMore ? data[data.length - 1].id : null,
hasMore,
},
});
}Include Related Data to Prevent N+1
Use an include query parameter to let clients request related data in a single call instead of making separate requests.
# Without include: 1 request for orders + N requests for users
GET /api/orders
# With include: 1 request with embedded user data
GET /api/orders?include=user,items- Cursor-based pagination is stable under concurrent writes
- Include parameter eliminates N+1 waterfall requests
- Consistent response shape simplifies frontend data handling
Contract-First Design
The most effective API workflow is contract-first: define the API shape (OpenAPI, tRPC router, GraphQL schema) before writing any implementation. Both teams work in parallel — backend implements the contract, frontend consumes mock data matching the contract.
- Define schemas in OpenAPI/Swagger, then generate TypeScript types automatically
- Use tools like MSW (Mock Service Worker) to mock APIs during frontend development
- Validate API responses against the contract in CI — catch drift before it reaches production
- tRPC eliminates the contract layer entirely by sharing types between server and client
Key Takeaways
- 1Design URLs as resources with HTTP methods — not RPC-style action endpoints
- 2Structured error responses with field-level details enable proper frontend error UIs
- 3Cursor-based pagination is more robust than offset-based for production applications
- 4Include related data to prevent N+1 request waterfalls from the frontend
- 5Contract-first design lets frontend and backend work in parallel with confidence
