GraphQL vs REST in 2025: Why APIs Are Changing
The war between GraphQL and REST APIs is far from over, but in 2025, the landscape has become much clearer. Companies like Netflix, Shopify, GitHub, and Airbnb have migrated significant parts of their APIs to GraphQL - and there are concrete reasons for this.
But here's the twist: REST isn't dead. In fact, for many use cases, REST remains the smarter choice. So, how do you know which to use? And more importantly: how do you implement each efficiently in Node.js?
Let's dive deep into this comparison with practical examples you can use today.
REST: The Classic That Still Dominates
REST (Representational State Transfer) is the established standard for decades. Its simplicity and predictability are its greatest strengths:
// Traditional REST API with Express.js
import express from 'express';
import { body, validationResult } from 'express-validator';
const app = express();
app.use(express.json());
// In-memory database (in production, use a real database)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', posts: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', posts: [3] }
];
let posts = [
{ id: 1, title: 'GraphQL Intro', authorId: 1, comments: [1] },
{ id: 2, title: 'REST Best Practices', authorId: 1, comments: [] },
{ id: 3, title: 'Node.js Performance', authorId: 2, comments: [2, 3] }
];
let comments = [
{ id: 1, text: 'Great article!', postId: 1, authorId: 2 },
{ id: 2, text: 'Very helpful', postId: 3, authorId: 1 },
{ id: 3, text: 'Thanks for sharing', postId: 3, authorId: 1 }
];
// GET /api/users - List all users
app.get('/api/users', (req, res) => {
const { limit = 10, offset = 0 } = req.query;
const paginatedUsers = users.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
res.json({
data: paginatedUsers,
pagination: {
total: users.length,
limit: parseInt(limit),
offset: parseInt(offset)
}
});
});
// GET /api/users/:id - Get specific user
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
});
// GET /api/users/:id/posts - User's posts
app.get('/api/users/:id/posts', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const userPosts = posts.filter(p =>
user.posts.includes(p.id)
);
res.json({ data: userPosts });
});
// POST /api/users - Create new user
app.post(
'/api/users',
[
body('name').trim().isLength({ min: 2 }).escape(),
body('email').isEmail().normalizeEmail()
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email,
posts: []
};
users.push(newUser);
res.status(201).json({ data: newUser });
}
);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API running on port ${PORT}`);
});The classic REST problem? Over-fetching and under-fetching. To get a user with their posts and comments, you need multiple requests:
// Client consuming REST API - N+1 problem
async function getUserWithPostsAndComments(userId) {
// Request 1: Fetch user
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// Request 2: Fetch user's posts
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const userPosts = await postsResponse.json();
// Requests 3-N: Fetch comments for each post
const postsWithComments = await Promise.all(
userPosts.data.map(async (post) => {
const commentsResponse = await fetch(`/api/posts/${post.id}/comments`);
const comments = await commentsResponse.json();
return { ...post, comments: comments.data };
})
);
return {
...user.data,
posts: postsWithComments
};
}
// 1 + 1 + N requests = Performance problem!
GraphQL: Flexibility and Efficiency
GraphQL solves exactly this problem. With a single query, you fetch exactly the data you need:
// GraphQL API with Apollo Server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Define GraphQL schema
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
post: Post!
author: User!
}
type Query {
users(limit: Int, offset: Int): [User!]!
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
}
`;
// Same data from REST example
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', postIds: [1, 2] },
{ id: 2, name: 'Bob', email: 'bob@example.com', postIds: [3] }
];
let posts = [
{ id: 1, title: 'GraphQL Intro', authorId: 1, commentIds: [1] },
{ id: 2, title: 'REST Best Practices', authorId: 1, commentIds: [] },
{ id: 3, title: 'Node.js Performance', authorId: 2, commentIds: [2, 3] }
];
let comments = [
{ id: 1, text: 'Great article!', postId: 1, authorId: 2 },
{ id: 2, text: 'Very helpful', postId: 3, authorId: 1 },
{ id: 3, text: 'Thanks for sharing', postId: 3, authorId: 1 }
];
// Resolvers - how to fetch data
const resolvers = {
Query: {
users: (_, { limit = 10, offset = 0 }) => {
return users.slice(offset, offset + limit);
},
user: (_, { id }) => {
return users.find(u => u.id === parseInt(id));
},
posts: () => posts,
post: (_, { id }) => {
return posts.find(p => p.id === parseInt(id));
}
},
User: {
posts: (parent) => {
return posts.filter(p => parent.postIds.includes(p.id));
}
},
Post: {
author: (parent) => {
return users.find(u => u.id === parent.authorId);
},
comments: (parent) => {
return comments.filter(c => parent.commentIds.includes(c.id));
}
},
Comment: {
post: (parent) => {
return posts.find(p => p.id === parent.postId);
},
author: (parent) => {
return users.find(u => u.id === parent.authorId);
}
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = {
id: users.length + 1,
name,
email,
postIds: []
};
users.push(newUser);
return newUser;
},
updateUser: (_, { id, name, email }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) {
throw new Error('User not found');
}
users[userIndex] = {
...users[userIndex],
...(name && { name }),
...(email && { email })
};
return users[userIndex];
},
deleteUser: (_, { id }) => {
const userIndex = users.findIndex(u => u.id === parseInt(id));
if (userIndex === -1) return false;
users.splice(userIndex, 1);
return true;
}
}
};
// Create Apollo server
const server = new ApolloServer({
typeDefs,
resolvers
});
// Start server
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
});
console.log(`GraphQL server running at ${url}`);Now, on the client side, a single query solves everything:
// GraphQL Client - ONE request!
const GET_USER_WITH_DATA = `
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}
`;
async function getUserWithPostsAndComments(userId) {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: GET_USER_WITH_DATA,
variables: { userId }
})
});
const { data } = await response.json();
return data.user;
}
// Only 1 request - much more efficient!
const user = await getUserWithPostsAndComments('1');
console.log(user);
When to Use REST vs GraphQL
The choice isn't binary. Here's a practical guide:
Use REST when:
- Simple public API: Public APIs with few endpoints (e.g., webhooks)
- Caching is critical: REST benefits from HTTP caching (CDNs, browsers)
- Small team: Less complexity to maintain
- Simple CRUD: Basic operations without complex relationships
- Predictable performance: Consistent latency and throughput
Use GraphQL when:
- Multiple clients: Mobile, web, desktop with different needs
- Related data: Entities with many relationships
- Agile development: Schema evolves rapidly
- Over-fetching is a problem: Bandwidth is limited (mobile)
- Developer experience: Large teams benefit from strong typing
Advanced Patterns: Best of Both Worlds
In 2025, many companies use a hybrid approach. Here's an example of combining REST and GraphQL:
// Hybrid: REST for simple operations, GraphQL for complex queries
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// REST endpoints for simple and public operations
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: Date.now() });
});
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
// Webhook processing (REST is better here)
const signature = req.headers['stripe-signature'];
// Process Stripe event
res.sendStatus(200);
});
// GraphQL for complex and related queries
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI()
}
})
});
await apolloServer.start();
// Mount GraphQL endpoint
app.use('/graphql', expressMiddleware(apolloServer));
// Differentiated rate limiting
const restLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
const graphqlLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 50
});
app.use('/api/*', restLimiter);
app.use('/graphql', graphqlLimiter);
app.listen(4000, () => {
console.log('Hybrid API server running on port 4000');
});DataLoader to Avoid N+1 Queries
One common problem in GraphQL is the N+1 issue. DataLoader solves this:
import DataLoader from 'dataloader';
// DataLoader for automatic query batching
class UserAPI {
constructor() {
this.loader = new DataLoader(async (userIds) => {
console.log(`Batch loading users: ${userIds.join(', ')}`);
// Single query for multiple IDs
const users = await db.users.findMany({
where: { id: { in: userIds } }
});
// Return in same order as IDs
return userIds.map(id =>
users.find(user => user.id === id)
);
});
}
async getUser(id) {
return this.loader.load(id);
}
}
// Use in resolver
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// Multiple calls will be automatically batched!
return dataSources.userAPI.getUser(parent.authorId);
}
}
};Performance and Monitoring
Both REST and GraphQL need proper monitoring:
// Performance middleware for REST
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`
});
});
next();
});
// Apollo plugin for GraphQL monitoring
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ request }) {
const duration = Date.now() - start;
console.log({
operation: request.operationName,
duration: `${duration}ms`
});
if (duration > 1000) {
console.warn(`Slow query: ${request.operationName}`);
}
}
};
}
}
]
});
The Future of APIs in 2025 and Beyond
Evolution doesn't stop. The most exciting trends include:
- GraphQL Federation: Microservices with unified schema
- REST with JSON:API: Standardization of REST responses
- tRPC: Type-safe APIs without explicit schema
- gRPC-Web: gRPC performance in the browser
- Server-Driven UI: APIs that return components, not just data
The consensus in 2025? There's no absolute winner. The best architectures combine different approaches based on the specific use case. GraphQL shines in complex applications with multiple clients, while REST remains unbeatable for simple and public APIs.
The important thing is to understand the trade-offs and choose the right tool for the right problem. As a modern developer, mastering both approaches makes you much more valuable in the market.
If you want to dive deeper into API architecture, I recommend reading my article about Microservices with Node.js: Modern Architecture in 2025 where I explore communication patterns between services.
Let's go! 🦅
💻 Master JavaScript for Real
The knowledge you gained in this article is just the beginning. There are techniques, patterns, and practices that transform beginner developers into sought-after professionals.
Invest in Your Future
I've prepared complete material for you to master JavaScript:
Payment options:
- $4.90 (single payment)

