API design has seen an ongoing debate since 2015: GraphQL or REST? Both have deep production track records; the difference is philosophical. This article compares the two approaches with concrete criteria and shows which one fits which scenario.
The Core Difference
Related guides: REST API security · API rate limiting strategies · KEYDAL API services
REST: resource-oriented (/users/42, /users/42/orders), HTTP verbs (GET/POST/PUT/DELETE), each endpoint returns a fixed response shape. GraphQL: a query language — the client says which fields it wants and the server returns exactly those. A single endpoint (/graphql), with queries sent over POST.
# REST
GET /users/42
→ { id, name, email, phone, address, createdAt, ... }
GET /users/42/orders
→ [{ id, total, items, createdAt }, ...]
# GraphQL
POST /graphql
query {
user(id: 42) {
name
email
orders(last: 10) {
id
total
}
}
}
→ { data: { user: { name, email, orders: [...] } } }
GraphQL Advantages
- No over-fetching: a mobile app needing only
nameandavatardoesn't download a 50-field user object - No under-fetching: fetch user + orders + items in a single request instead of three round trips
- Strongly typed schema: type safety via SDL (Schema Definition Language)
- Tooling: Apollo Studio, GraphiQL — auto-complete and auto-generated docs
- Subscriptions: real-time updates over WebSocket
GraphQL Drawbacks
- Caching is hard: single POST endpoint — HTTP caches don't help, you need a client-side cache
- N+1 problem: naïve resolvers hammer the DB — DataLoader is a must
- Complexity: schema, resolvers, data sources — more moving parts than REST
- Monitoring is harder: 1,000 distinct queries through a single endpoint — no route-based metrics
- Uncontrolled query cost: a malicious deeply-nested query can take the server down
REST Advantages
- Simple: every developer knows it, the learning curve is low
- HTTP semantics: status codes, methods, headers — built-in
- Caching is easy:
Cache-Control, CDN and browser cache just work - Tool ecosystem: curl, Postman, OpenAPI, SDK generators
- Versioning is easy:
/v1/users,/v2/users
The N+1 Problem and DataLoader
// Naïve resolver — N+1
const resolvers = {
Post: {
author: (post) => db.users.findByPk(post.authorId)
}
};
// Listing 100 posts triggers 100 separate user queries → DB melts
// DataLoader for batching + caching
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (ids) => {
const users = await db.users.findAll({ where: { id: ids } });
return ids.map(id => users.find(u => u.id === id));
});
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId)
}
};
// 100 posts → a single DB query (WHERE id IN [...])
Schema Design (SDL)
type User {
id: ID!
email: String!
name: String
posts(limit: Int = 10, after: ID): PostConnection!
role: Role!
}
enum Role { USER ADMIN MODERATOR }
type Post {
id: ID!
title: String!
content: String!
author: User!
createdAt: DateTime!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type Query {
me: User
user(id: ID!): User
posts(limit: Int = 10): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
input CreatePostInput {
title: String!
content: String!
}
Query Cost Controls
// Query depth limit
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs, resolvers,
validationRules: [depthLimit(5)] // max depth of 5
});
// Query complexity
const { createComplexityLimitRule } = require('graphql-validation-complexity');
validationRules: [createComplexityLimitRule(1000)];
// Persisted queries — only whitelisted queries are accepted
const persistedQueries = { 'abc123': 'query { me { id } }' };
Which One, When?
Pick GraphQL when:
- Mobile app + web app need different data from the same backend
- Lots of nested resources: user → orders → items → products → reviews
- Large frontend team: let them write their own queries without backend changes
- Real-time features via subscriptions
- In a BFF (backend-for-frontend) pattern
Pick REST when:
- Public API: consumed by third-party integrators — REST is universal
- Simple CRUD: admin panels, dashboards — GraphQL is overkill
- CDN caching is critical: public content, high traffic
- Service-to-service microservices: REST/gRPC tend to be better
- The team's experience is in REST
Hybrid Approach
Most modern apps use both: GraphQL for the client-facing API, REST/gRPC for internal microservice communication. In the BFF pattern, a GraphQL gateway calls REST services behind it.
Alternative: tRPC
If you're in a TypeScript monorepo and don't want GraphQL's complexity but still want REST's flexibility, check out tRPC. No schema — TypeScript types give you end-to-end type safety. It's Apollo's lightweight rival.
API Design Principles and Secure Endpoint Architecture
Professional API design combines four elements: protocol choice (REST for CRUD, GraphQL for flexible queries, gRPC for microservice-to-microservice), authentication (OAuth 2.0 / OIDC, JWT access tokens, refresh token rotation), rate limiting (token bucket, sliding window, per-IP/user/API key) and versioning (URL versioning /v1/, header versioning, deprecation processes). API endpoint security uses input validation, prepared statements, CORS policy, idempotency-key (for POST), webhook signature verification and OpenAPI/Swagger documentation. For high-traffic APIs, use Redis for rate limit and cache, Kafka or RabbitMQ for async job queues, and OpenTelemetry for distributed tracing.
Reach out to KEYDAL for GraphQL, REST or tRPC selection and API architecture. Contact us