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

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 name and avatar doesn'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.

Conclusion

"Which is better?" is the wrong question — both are modern and production-ready. When ecosystem and team experience are neutral: pick GraphQL for internal API + rapid product iteration, REST for public API + simple CRUD. Also consider tRPC for TypeScript monorepos.

API design consulting

Reach out to KEYDAL for GraphQL, REST or tRPC selection and API architecture. Contact us

WhatsApp