How GraphQL Changed API Design
GraphQL reshaped how we build APIs by flipping control from server to client. This article explores its trade-offs: the elegance of type systems and precise queries versus the pain of N+1 queries, caching complexity, and exponential logic.
How GraphQL Changed API Design (for Better and for Worse)
Back in 2015, when Facebook open-sourced GraphQL, most of us in the Python community were happily building REST APIs with Flask or Django REST Framework. Life was simple. You had your endpoints, your JSON responses, and if you needed to fetch a user and their posts, you either made two requests or built a custom endpoint. Nobody complained too loudly.
But then GraphQL walked in and asked a deceptively simple question: Why should the server decide what data the client gets?
That question turned API design on its head. Let me walk you through what actually changed.
The Old Way: REST's Fixed Response Problem
Before GraphQL, if you were building a Python API for Pythonskillset.com, you'd probably have something like this:
# A typical REST endpoint
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
user = User.query.get(user_id)
return jsonify({
'id': user.id,
'name': user.name,
'email': user.email,
'bio': user.bio,
'avatar_url': user.avatar_url,
'join_date': user.join_date,
'total_articles': user.total_articles,
# ... 15 more fields
})
The problem? The mobile app might only need name and avatar_url, but the admin dashboard needs everything. You're either over-fetching (wasting bandwidth) or under-fetching (requiring multiple calls). Neither is great at scale.
What GraphQL Actually Changed
1. One Endpoint to Rule Them All
This is the change everyone notices first. Instead of /api/users, /api/posts, /api/comments, you get one endpoint — usually /graphql. Everything goes through it.
# Client says exactly what they want
query {
user(id: 42) {
name
avatarUrl
recentPosts(limit: 5) {
title
publishedAt
}
}
}
The server returns only the name, avatarUrl, and the title and publishedAt of the last five posts. Nothing more. For a mobile app on a slow connection, this is transformational.
2. The Type System (This is the Real Game Changer)
REST APIs usually document their responses, but the client finds out about mismatches at runtime. GraphQL forces you to define everything upfront with a schema:
# Using Graphene (Python GraphQL library)
class UserType(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
avatar_url = graphene.String()
recent_posts = graphene.List(PostType, limit=graphene.Int(default_value=10))
This schema becomes a contract between frontend and backend. If you change a field name, the client's query breaks at development time, not in production. For a site like Pythonskillset.com with multiple frontend teams, this saved countless debugging hours.
3. The New Problem: N+1 Queries
Here's where the "for worse" part comes in. In REST, you know exactly which endpoints get called. In GraphQL, a single query can trigger dozens of database queries if you're not careful.
Consider this innocent-looking query:
query {
articles(limit: 20) {
title
author {
name
bio
}
comments {
text
author {
name
}
}
}
}
Without optimization, this could mean: - 1 query for 20 articles - 20 queries for article authors - 20 queries for article comments - 200 queries for comment authors (20 articles x 10 comments each)
That's 241 database queries for a single API call. Facebook solved this with DataLoader, and Python has its own implementation (aiodataloader) that batches these requests:
class AuthorLoader(DataLoader):
def batch_load_fn(self, keys):
authors = Author.query.filter(Author.id.in_(keys)).all()
return [authors_by_id.get(key) for key in keys]
# Now the 20 author queries become 1
4. Caching Got Weird
REST APIs are beautiful for caching. The URL /api/user/42 can be cached by any HTTP cache. GraphQL? Every query hits the same endpoint. You can't cache based on URL alone.
The community eventually figured out solutions — persisted queries, automatic persisted queries (APQ), and client-side normalization (like Apollo Client's cache). But it's not as clean as REST's HTTP caching.
5. The Real Cost: Complexity
GraphQL solves the "fetch exactly what you need" problem, but introduces what I call the "exponential complexity" problem.
With REST, your API logic is spread across endpoints. Each endpoint is simple and focused. With GraphQL, a single resolver could be responsible for data that previously lived in 5 different endpoints. Debugging becomes harder. Rate limiting becomes trickier. Authorization requires granular thinking (can a user query user.secretField even if they don't ask for it in the query?).
When Should You Actually Use GraphQL?
After years of building APIs for Pythonskillset.com, here's my honest take:
Use GraphQL when: - You have multiple client types (web, mobile, third-party) - Clients need different data shapes - You have complex data relationships - Your team has strong frontend and backend engineers
Stick with REST when: - Your API is simple (CRUD on a few resources) - You only have one client - You need maximum caching efficiency - Your team is small and spread thin
The Lasting Legacy
GraphQL didn't kill REST, and it wasn't supposed to. What it did was force us all to think harder about our APIs. REST APIs got better because of GraphQL — they started offering field selection, better documentation, and more flexible querying.
The biggest change? It moved API design from "my way or the highway" to a conversation between client and server. And for Python developers specifically, libraries like Strawberry and Ariadne have made implementing GraphQL servers feel almost as natural as building REST APIs with Flask.
GraphQL changed API design by asking one uncomfortable question: "Why are you sending me data I didn't ask for?" The answer has made us all better API designers — even those of us who still mostly build REST APIs.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.