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 resourcesWhen 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.