Back to Insights
API DesignRESTDX

Modern API Design for Frontend Developers

11/20259 min read
Share
Modern API Design for Frontend Developers

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

api-design.md
# ❌ 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
Result
  • 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

app/api/users/route.ts
// 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:

hooks/useFormSubmit.ts
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 }
Result
  • 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

app/api/users/route.ts
// 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.

Usage
# 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
Result
  • 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