Back to Blog
RESTGraphQLAPIArchitecture

REST vs GraphQL: When to Use Which

Compare REST and GraphQL objectively. From use cases to performance to developer experience trade-offs.

B
Bootspring Team
Engineering
June 5, 2022
6 min read

REST and GraphQL solve API design differently. Here's how to choose the right approach for your project.

Core Differences#

REST: - Multiple endpoints (/users, /posts, /comments) - Fixed data shapes per endpoint - HTTP methods define operations (GET, POST, PUT, DELETE) - Stateless request/response GraphQL: - Single endpoint (/graphql) - Client specifies exact data needed - Query language defines operations - Strongly typed schema

REST Example#

1// Multiple endpoints for related data 2 3// GET /api/users/123 4{ 5 "id": "123", 6 "name": "John Doe", 7 "email": "john@example.com" 8} 9 10// GET /api/users/123/posts 11[ 12 { "id": "1", "title": "First Post", "userId": "123" }, 13 { "id": "2", "title": "Second Post", "userId": "123" } 14] 15 16// GET /api/posts/1/comments 17[ 18 { "id": "1", "content": "Great post!", "postId": "1" } 19] 20 21// Server implementation 22app.get('/api/users/:id', async (req, res) => { 23 const user = await db.user.findUnique({ 24 where: { id: req.params.id }, 25 }); 26 res.json(user); 27}); 28 29app.get('/api/users/:id/posts', async (req, res) => { 30 const posts = await db.post.findMany({ 31 where: { userId: req.params.id }, 32 }); 33 res.json(posts); 34});

GraphQL Example#

1# Single query for all related data 2 3query GetUserWithPosts($userId: ID!) { 4 user(id: $userId) { 5 id 6 name 7 email 8 posts { 9 id 10 title 11 comments { 12 id 13 content 14 author { 15 name 16 } 17 } 18 } 19 } 20} 21 22# Response - exactly what was requested 23{ 24 "data": { 25 "user": { 26 "id": "123", 27 "name": "John Doe", 28 "email": "john@example.com", 29 "posts": [ 30 { 31 "id": "1", 32 "title": "First Post", 33 "comments": [ 34 { 35 "id": "1", 36 "content": "Great post!", 37 "author": { "name": "Jane" } 38 } 39 ] 40 } 41 ] 42 } 43 } 44}
1// Server implementation 2const resolvers = { 3 Query: { 4 user: (_, { id }) => db.user.findUnique({ where: { id } }), 5 }, 6 User: { 7 posts: (user) => db.post.findMany({ where: { userId: user.id } }), 8 }, 9 Post: { 10 comments: (post) => db.comment.findMany({ where: { postId: post.id } }), 11 }, 12 Comment: { 13 author: (comment) => db.user.findUnique({ where: { id: comment.authorId } }), 14 }, 15};

When to Use REST#

1// REST excels at: 2 3// 1. Simple CRUD operations 4GET /api/users // List users 5POST /api/users // Create user 6GET /api/users/:id // Get user 7PUT /api/users/:id // Update user 8DELETE /api/users/:id // Delete user 9 10// 2. File uploads/downloads 11POST /api/files // Upload file 12GET /api/files/:id // Download file 13 14// 3. Caching with HTTP standards 15app.get('/api/products', (req, res) => { 16 res.set('Cache-Control', 'public, max-age=3600'); 17 res.set('ETag', productHash); 18 res.json(products); 19}); 20 21// 4. Public APIs with broad usage 22// Simpler to understand and integrate 23 24// 5. Microservices communication 25// Direct mapping to resources

When to Use GraphQL#

1// GraphQL excels at: 2 3// 1. Complex data requirements 4query Dashboard { 5 user { 6 name 7 notifications { unreadCount } 8 recentOrders(limit: 5) { 9 total 10 status 11 } 12 recommendations { 13 product { name, price } 14 } 15 } 16} 17 18// 2. Mobile applications (bandwidth sensitive) 19// Request only needed fields 20query MobileUserProfile { 21 user { 22 name 23 avatar // Only these two fields 24 } 25} 26 27// 3. Rapidly evolving frontends 28// Add fields without new endpoints 29type User { 30 id: ID! 31 name: String! 32 email: String! 33 avatar: String # Added later 34 preferences: JSON # Added later 35} 36 37// 4. Aggregating multiple services 38const resolvers = { 39 User: { 40 orders: (user) => orderService.getByUser(user.id), 41 payments: (user) => paymentService.getByUser(user.id), 42 notifications: (user) => notificationService.getByUser(user.id), 43 }, 44}; 45 46// 5. Real-time subscriptions 47subscription OnNewMessage($chatId: ID!) { 48 messageAdded(chatId: $chatId) { 49 id 50 content 51 sender { name } 52 } 53}

Performance Comparison#

1// REST: N+1 problem on client 2async function getUserWithPosts(userId: string) { 3 // 1 request 4 const user = await fetch(`/api/users/${userId}`); 5 6 // 1 request per user's posts 7 const posts = await fetch(`/api/users/${userId}/posts`); 8 9 // N requests for comments 10 const comments = await Promise.all( 11 posts.map(post => fetch(`/api/posts/${post.id}/comments`)) 12 ); 13 14 // Total: 2 + N requests 15} 16 17// GraphQL: N+1 problem on server (solved with DataLoader) 18const userLoader = new DataLoader(async (ids) => { 19 const users = await db.user.findMany({ 20 where: { id: { in: ids } }, 21 }); 22 return ids.map(id => users.find(u => u.id === id)); 23}); 24 25const resolvers = { 26 Comment: { 27 author: (comment) => userLoader.load(comment.authorId), 28 }, 29};

Caching Strategies#

1// REST: HTTP caching is straightforward 2// - Browser cache 3// - CDN cache 4// - Cache-Control headers 5// - ETags 6 7// GraphQL: More complex caching 8// 1. Response caching (limited) 9// 2. Normalized cache (Apollo Client) 10const cache = new InMemoryCache({ 11 typePolicies: { 12 User: { 13 keyFields: ['id'], 14 }, 15 Post: { 16 keyFields: ['id'], 17 }, 18 }, 19}); 20 21// 3. Persisted queries (performance + caching) 22// Client sends query hash instead of full query 23POST /graphql 24{ 25 "extensions": { 26 "persistedQuery": { 27 "sha256Hash": "abc123..." 28 } 29 }, 30 "variables": { "id": "123" } 31}

Error Handling#

1// REST: HTTP status codes 2// 200 OK 3// 400 Bad Request 4// 401 Unauthorized 5// 404 Not Found 6// 500 Internal Server Error 7 8// GraphQL: Always 200, errors in response 9{ 10 "data": { 11 "user": null 12 }, 13 "errors": [ 14 { 15 "message": "User not found", 16 "path": ["user"], 17 "extensions": { 18 "code": "NOT_FOUND" 19 } 20 } 21 ] 22} 23 24// Partial success possible in GraphQL 25{ 26 "data": { 27 "user": { 28 "name": "John", 29 "posts": null // Failed but user succeeded 30 } 31 }, 32 "errors": [ 33 { "message": "Failed to fetch posts", "path": ["user", "posts"] } 34 ] 35}

Hybrid Approach#

1// Use both where appropriate 2 3// REST for: 4// - File uploads 5// - Webhooks 6// - Simple public APIs 7// - Health checks 8 9// GraphQL for: 10// - Dashboard data 11// - Complex queries 12// - Mobile apps 13// - Internal APIs 14 15// Same server 16app.use('/api', restRouter); 17app.use('/graphql', graphqlServer); 18 19// REST endpoints can use GraphQL internally 20app.get('/api/dashboard', async (req, res) => { 21 const result = await graphql({ 22 schema, 23 source: dashboardQuery, 24 contextValue: { user: req.user }, 25 }); 26 res.json(result.data); 27});

Comparison Summary#

| Aspect | REST | GraphQL | |-----------------|----------------------|----------------------| | Endpoints | Multiple | Single | | Data fetching | Server decides | Client decides | | Versioning | URL or header | Schema evolution | | Caching | HTTP native | Custom solutions | | File upload | Native support | Requires setup | | Learning curve | Lower | Higher | | Tooling | Mature | Growing rapidly | | Over-fetching | Common | Solved | | Under-fetching | Common | Solved | | Type safety | Optional (OpenAPI) | Built-in |

Best Practices#

Choose REST when: ✓ Simple CRUD operations ✓ Public APIs ✓ Heavy caching needs ✓ File operations ✓ Team new to APIs Choose GraphQL when: ✓ Complex data relationships ✓ Multiple clients (mobile, web) ✓ Rapid frontend iteration ✓ Real-time requirements ✓ Internal APIs

Conclusion#

Neither REST nor GraphQL is universally better. REST offers simplicity and mature tooling; GraphQL provides flexibility and efficiency for complex data needs. Consider your team's experience, client requirements, and caching needs. Often, a hybrid approach works best.

Share this article

Help spread the word about Bootspring