GraphQL vs REST: API Design Patterns and Performance Considerations

REST Architecture and N+1 Query Problem

REST: resource-based endpoints. GET /users/123 returns { id, name, email, company_id }. To get company details, second request: GET /companies/456 returns company data. Mobile app fetches user profile: request 1 (user), request 2 (company), request 3 (company address), request 4 (company employees) (4 requests, 4 round-trips, 200-500ms total latency). Backend inefficiency: N+1 problem. Query all users (1 query), then for each user load company (N queries). Example: SELECT * FROM users (1000 rows), then for each: SELECT * FROM companies WHERE id = ? (1000 queries). Total: 1001 queries. Solution: JOIN or batch loading. GraphQL avoids this: query { users { name, company { name, address } } } (single request, backend resolves relationships in optimal way via dataloader). Over-fetching: REST returns all user fields (id, name, email, phone, address, ...), client uses only name. Payload 500 bytes vs needed 50 bytes (10x overhead). Under-fetching: client needs data spread across 3 endpoints (makes 3 requests).

GraphQL Query Language and Schema

GraphQL query: query { user(id: 123) { name, posts { title, createdAt } } }. Request specifies exact fields needed (zero over-fetching). Server returns { user: { name: "John", posts: [{ title: "Hello", createdAt: "2024-01-01" }] } } (exact structure). Schema defines types: type User { id: ID!, name: String!, email: String, posts: [Post!]! }. Exclamation mark (!) = required field. Arrays: [Post!] = array of non-null Posts. Mutations: mutation { createPost(title: "New", content: "...") { id, createdAt } } (create/update/delete). Subscriptions: subscription { userCreated { id, name } } (real-time updates via WebSocket). Aliases: query { me: user(id: 1) { name }, admin: user(id: 2) { name } } (fetch multiple users with same query). Fragments: fragment UserFields on User { id, name, email }, query { user(id: 1) { ...UserFields } } (reusable field sets).

Performance Optimization and Caching

REST caching: HTTP cache headers (cache-control: max-age=3600). GET /users/123 cached by browser/CDN (same ID always returns same data). GraphQL caching harder: POST /graphql with body (cache-busting), no HTTP caching by default. Solution: Persisted queries (send query ID, not full query), HTTP GET transport (enable caching). Query complexity: complex queries expensive (deeply nested, large result sets). Example: query { users { posts { comments { author { posts { comments { ... (infinite nesting, DOS attack). Solution: query depth limit (max 5 levels), query cost limit (assign point values to fields, reject if total > threshold). Dataloader pattern: batches database queries. Without: N users → N database queries. With dataloader: collect all user IDs → single database query (SELECT * FROM users WHERE id IN (...)). Performance: 1000 requests → 100ms per request (REST) vs 1-2 database batches (~5ms via dataloader). Field resolution: resolver functions fetch data. Lazy loading: only fetch fields requested by client. Field-level authorization: resolver checks permissions (some fields hidden for non-admin).

REST vs GraphQL Trade-offs

REST strengths: simple (GET = read, POST = create), HTTP caching standard (client/CDN/proxy cache), stateless (easy horizontal scaling), versioning (api.v1, api.v2). Mature ecosystem: OpenAPI/Swagger, thousands of frameworks. GraphQL strengths: single endpoint (no versioning needed, backward compatible), typed schema (introspection, auto-generated docs), client-driven queries (mobile requests less data than web). REST weaknesses: N+1 queries, over/under-fetching, versioning complexity, API evolution breaks clients. GraphQL weaknesses: query complexity (DOS attacks), caching complexity (POST request), learning curve, monitoring harder (all requests same endpoint). Cost: REST easy to optimize (cache popular endpoints), GraphQL requires dataloader + query depth limits + monitoring. Real-world: GraphQL 5-10% of requests at Facebook (most traffic REST). Mobile apps benefit most from GraphQL (reduced bandwidth). Backend infrastructure: GraphQL requires more processing (parse query, validate schema, resolve fields). Example: simple REST endpoint ~1ms latency, GraphQL endpoint ~5-10ms (overhead worth savings in bandwidth/round-trips).

GraphQL Implementation and Tools

Apollo Server: Node.js GraphQL server. TypeDefs define schema, resolvers provide data. const server = new ApolloServer({ typeDefs, resolvers }). Resolvers: function returning data. resolvers: { Query: { user: (parent, args) => db.user(args.id) } }. Context: share data across resolvers (authentication, database connection). middleware: auth in context, each resolver accesses req.user. Federation: combine multiple GraphQL services. Example: User service (handles users), Post service (handles posts), Apollo Gateway routes queries to appropriate service. Subscriptions: WebSocket transport. Server push updates to client (real-time notifications). Code-first vs schema-first: code-first (TypeGraphQL generates schema from TypeScript classes), schema-first (write schema, generate types). Performance tuning: caching resolvers (@Cacheable decorator), batching (DataLoader), async resolvers (parallel resolution). Monitoring: Apollo Studio tracks query performance, errors, usage patterns per client.

Migration from REST to GraphQL

Coexistence: run REST + GraphQL simultaneously. REST clients continue working (backward compatible). GraphQL clients use new API (benefits immediately). Gradual migration: create GraphQL gateway wrapping REST services (no backend changes needed). Example: User REST endpoint → GraphQL schema type User wraps REST call. Transition: months-long migration (users adopt GraphQL at own pace). Deprecation: disable old REST endpoints after all clients migrate. Monitoring: observe which endpoints called (identify migration blockers). Benefits realized: mobile apps reduce data usage 50-80% (GraphQL vs REST). Desktop web: minimal difference (network bandwidth not bottleneck). Internal services: GraphQL reduces integration complexity (single query vs 3-5 REST calls). Long-term: GraphQL enables faster feature development (clients request new fields, backend adds to schema). Breaking changes: add new schema field (not breaking), remove field (breaking if clients use). Versioning still needed for major breaking changes (but less frequent than REST).

Practical Implementation Patterns

Authentication: JWT token in Authorization header (same as REST). Per-field authorization: check permissions in resolver (admin-only fields). Rate limiting: GraphQL-specific (query cost limit). Example: simple query 1 point, field with database call 10 points, nested query 20 points, limit 1000 points/minute per client. Batch requests: send multiple queries in single request (reduce round-trips). Subscriptions: WebSocket connection persists (client sends subscribe command, server sends updates when data changes). Example: subscription onUserCreated { id, name } triggers whenever new user created. Use case: real-time dashboards, collaborative editors, live notifications. Error handling: partial success (some fields fail, others succeed). Example: query { user { id, name, posts { error, data } } } (post fetch fails, but user data still returns). Debugging: GraphQL playground (interactive explorer) or Apollo DevTools. Query introspection: GET /graphql?__schema (clients discover schema, generate code).