- Published on
REST vs RPC vs GraphQL: A Frontend Developer's Guide to API Architecture
- Authors

- Name
- Mohit Verma
Hey frontend folks! π Let's talk about something we deal with daily but often don't think too deeply about - API architectures. I've worked with all three major approaches (REST, RPC, and GraphQL), and today I'm sharing my real-world experiences with each one.
Quick Overview: The Three Musketeers π€Ί
First, let's break down each approach with a simple example - fetching user data:
// REST - Resource-focused
GET /api/users/123
GET /api/users/123/posts
GET /api/users/123/followers
// RPC - Action-focused
POST /api/getUserById({ id: 123 })
POST /api/getUserPosts({ userId: 123 })
POST /api/getUserFollowers({ userId: 123 })
// GraphQL - Query-focused
query {
user(id: 123) {
name
posts {
title
content
}
followers {
name
avatar
}
}
}
REST: The Familiar Friend π€
Pros:
- Widely Adopted
// Most developers are familiar with this
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
- Caching is Straightforward
// Browser naturally caches GET requests
fetch('/api/users/123', {
headers: {
'Cache-Control': 'max-age=3600'
}
});
Cons:
- Overfetching/Underfetching
// Need just the username? Too bad, you get everything
const user = await fetch('/api/users/123').then(r => r.json());
console.log(user.name); // But we received full user object
- Multiple Requests for Related Data
// The dreaded waterfall
const user = await fetch('/api/users/123').then(r => r.json());
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
const followers = await fetch(`/api/users/${user.id}/followers`).then(r => r.json());
RPC: The Action Hero π¦ΈββοΈ
Pros:
- Intuitive for Action-Based Operations
// Clear what's happening
const banUser = async (userId) => {
return await rpc.call('banUser', { userId });
};
const sendFriendRequest = async (fromId, toId) => {
return await rpc.call('sendFriendRequest', { fromId, toId });
};
- Great for Complex Operations
// Multiple steps in one call
const processOrder = async (orderId) => {
return await rpc.call('processOrder', {
orderId,
steps: ['validate', 'payment', 'inventory', 'shipping']
});
};
Cons:
- Less Predictable
// What does this actually do?
await rpc.call('handleUser', { userId: 123 });
// Naming becomes crucial for understanding
- Harder to Cache
// Most RPC calls use POST
const getData = async () => {
return await rpc.call('getData', { /* params */ });
// No automatic browser caching
};
GraphQL: The New Kid on the Block π
Pros:
- Precise Data Fetching
const UserProfile = () => {
const { data } = useQuery(gql`
query GetUser($id: ID!) {
user(id: $id) {
name
avatar
# Only what we need!
}
}
`);
return <div>{data.user.name}</div>;
};
- One Request for Multiple Resources
const DashboardData = () => {
const { data } = useQuery(gql`
query GetDashboard {
user {
name
posts {
title
}
followers {
name
}
}
notifications {
message
}
stats {
views
}
}
`);
// All data in one request!
};
Cons:
- Learning Curve
// Need to learn query language
const COMPLEX_QUERY = gql`
query GetUserData($id: ID!, $first: Int!) {
user(id: $id) {
...UserFields
posts(first: $first) {
edges {
node {
...PostFields
}
}
}
}
}
${USER_FIELDS}
${POST_FIELDS}
`;
- Can Be Overkill for Simple APIs
// Do we really need GraphQL for this?
query {
ping
}
// REST would be simpler:
GET /api/ping
Real-World Decision Making π€
Here's when I choose each:
REST
// Perfect for CRUD operations
const api = {
getUser: (id) => fetch(`/users/${id}`),
createUser: (data) => fetch('/users', {
method: 'POST',
body: JSON.stringify(data)
}),
updateUser: (id, data) => fetch(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
}),
deleteUser: (id) => fetch(`/users/${id}`, {
method: 'DELETE'
})
};
RPC
// Great for action-based operations
const gameApi = {
startMatch: (players) => rpc.call('startMatch', { players }),
makeMove: (gameId, move) => rpc.call('makeMove', { gameId, move }),
forfeitGame: (gameId, playerId) => rpc.call('forfeitGame', { gameId, playerId })
};
GraphQL
// Perfect for complex, nested data
const SocialFeed = () => {
const { data } = useQuery(gql`
query GetFeed {
feed {
posts {
author {
name
followers {
count
}
}
comments {
text
likes
}
shares {
count
}
}
}
}
`);
};
My Personal Decision Framework π―
I choose:
REST when:
- Building simple CRUD applications
- Need good caching
- Working with a traditional backend team
RPC when:
- Building action-heavy applications (games, trading)
- Need simple, direct function calls
- Performance is crucial
GraphQL when:
- Building data-heavy applications
- Need flexible data fetching
- Working with multiple data sources
Practical Tips for Each π‘
REST Tips
// Use axios for better DX
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
});
// Organize by resources
const userApi = {
get: (id) => api.get(`/users/${id}`),
getPosts: (id) => api.get(`/users/${id}/posts`)
};
RPC Tips
// Create type-safe RPC clients
interface RPCMethods {
startGame(params: StartGameParams): Promise<GameState>;
makeMove(params: MoveParams): Promise<MoveResult>;
}
const rpc = createRPCClient<RPCMethods>('/api');
GraphQL Tips
// Use fragments for reusable pieces
const USER_FIELDS = gql`
fragment UserFields on User {
id
name
avatar
status
}
`;
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
${USER_FIELDS}
`;
Conclusion
Remember:
- There's no "best" approach - only best for your use case
- You can mix approaches in the same project
- Start simple, add complexity when needed
- Consider your team's expertise
- Think about long-term maintenance
Choose wisely, and don't be afraid to change if needed! Happy coding! π
Practice Makes Perfect
Visit PrepareFrontend to start practicing frontend interview questions
