← All posts

Choosing the Right API: REST, GraphQL, or gRPC for Your Next Project

Choosing the right API—REST, GraphQL, or gRPC—can transform your backend’s performance, flexibility, and future‑proofing.

When a team starts a new backend project, the first design decision that surfaces is the API style.
Choosing between REST, GraphQL, or gRPC isn’t just a “tool‑of‑the‑trade” choice; it shapes how clients request data, how servers evolve, and how the whole system behaves under load.

TL;DR

  • REST: Simple CRUD, clear URLs, built‑in caching, best for public or slowly evolving services.
  • GraphQL: One endpoint, fine‑grained field selection, ideal when UI needs mixed or nested data.
  • gRPC: Binary protobuf, low latency, native streaming, suited for internal micro‑service mesh or real‑time workloads.

When to Use Each Style

REST

REST is the de‑facto standard for exposing resources over HTTP.
It shines when:

  • The domain consists of well‑defined entities (users, posts, orders).
  • Clients only need CRUD operations and the overhead of a full HTTP request/response cycle is acceptable.
  • You want to leverage HTTP caching, content‑negotiation, and mature tooling (e.g., Postman, Swagger).

GraphQL

GraphQL’s appeal lies in its request flexibility.
Use it when:

  • Clients (often front‑end apps) need to fetch a specific subset of fields or combine data from several resources in a single round‑trip.
  • You anticipate a rapidly evolving schema; adding new fields is non‑breaking.
  • You want a strongly‑typed schema that can be introspected and auto‑generated into client code.

gRPC

gRPC is a binary protocol built on HTTP/2, designed for efficient inter‑service communication.
It excels when:

  • Services must process thousands of requests per second with minimal latency.
  • You need bi‑directional streaming (e.g., real‑time chat, telemetry).
  • Your infrastructure is language‑agnostic; protobuf definitions can be compiled into many languages.

Performance & Bandwidth

REST

REST typically serializes entire objects as JSON.
A GET /users/42 might return a 200 KB payload if the user object contains nested logs, even though the client only needs name and email.
Over‑fetching wastes bandwidth and can degrade perceived performance, especially on mobile networks.

GraphQL

GraphQL lets the client specify exactly which fields to return:

query {
  user(id: 42) {
    name
    email
  }
}

The server responds with only those fields, often cutting payload size by 70–80 % in practice.
However, the server must still resolve each field, which can introduce overhead if resolvers are expensive.

gRPC

gRPC uses Protocol Buffers, a compact binary format.
Serialization and deserialization are faster than JSON, and the message size is usually a fraction of the JSON equivalent.
For example, a protobuf payload for the same user data might be ~1 KB.
When combined with HTTP/2 multiplexing, gRPC reduces connection setup overhead and improves throughput.


Data Shape & Client Flexibility

REST

REST relies on URLs to describe the resource hierarchy:
GET /users/42/posts/7.
Complex queries (e.g., “give me all posts by users in a certain city, sorted by date”) often require query strings or multiple endpoints, leading to a proliferation of endpoints and confusing semantics.

GraphQL

GraphQL exposes a single endpoint (/graphql) and a type system that maps directly to UI components.
A UI can request:

{
  posts(limit: 10, filter: { author: { city: "Berlin" } }) {
    id
    title
    author { name, city }
  }
}

The shape of the response mirrors the query, making it trivial for the front‑end to bind data to components without manual mapping.

gRPC

gRPC defines strict service contracts via .proto files:

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
}

Clients must be compiled against the same schema; they cannot request arbitrary fields.
This rigidity enforces consistency but reduces flexibility for ad‑hoc queries.


Tooling & Ecosystem

Feature REST GraphQL gRPC
Middleware Express, Koa, Hapi Apollo Server, GraphQL‑Yoga gRPC‑node, Envoy
Caching HTTP cache, CDN Custom caching, persisted queries gRPC interceptors
Debugging HTTP tools, Wireshark GraphiQL, introspection grpcurl, Envoy logs
Code Generation OpenAPI → TS GraphQL Code Generator → TS protoc → TS stubs
  • REST’s middleware ecosystem is vast; you can drop in authentication, rate limiting, or logging with minimal friction.
  • GraphQL’s introspection and schema stitching allow you to compose services from multiple domains.
  • gRPC’s built‑in streaming, deadline propagation, and load‑balancing via Envoy make it a natural fit for service meshes.

Development Flow

REST

Versioning is often handled through URLs (/v1/users) or custom headers.
Breaking changes (e.g., renaming a field) require careful migration and deprecation strategies.
Automated contract tests (e.g., Pact) can catch regressions early.

GraphQL

Adding a new field to a type is a non‑breaking change; old clients continue to work.
Removing a field, however, breaks clients that query it.
Best practices include:

  • Mark deprecated fields in the schema and provide a deprecation reason.
  • Use a schema registry to track changes across environments.

gRPC

gRPC enforces strict contract evolution.
Adding optional fields (optional string nickname = 4;) is safe; removing fields is not.
When a change is required, you typically create a new service version (UserServiceV2) and use version‑aware routing.


Common Mistakes & Trade‑offs

Category Mistake Consequence Mitigation
REST Over‑fetching via large resources Wasted bandwidth, slower UX Use pagination, selective fields, or GraphQL for complex queries
GraphQL Over‑engineering the schema Complex resolvers, slow server Keep schema flat, use data loaders to batch DB calls
gRPC Relying on raw protobufs without reflection Harder debugging, opaque logs Use Envoy, grpc‑web, or gRPC‑UI for inspection

Trade‑offs to consider

  • Simplicity vs. Flexibility: REST is simple but rigid; GraphQL is flexible but can become complex; gRPC is efficient but requires tight coupling.
  • Tooling vs. Performance: REST tools are abundant; gRPC offers higher performance at the cost of debugging overhead.
  • Client vs. Server Focus: GraphQL shifts complexity to the server (resolvers); REST keeps the server simple but pushes data shaping to the client.

Real‑World Example: CRUD Service in Three Styles

Below is a minimal “user” CRUD example written in TypeScript for Node.js.
It demonstrates the same operations expressed in REST, GraphQL, and gRPC.

// --------------------------------------------------
// REST (Express)
// --------------------------------------------------
import express from 'express';
const app = express();
app.use(express.json());

app.get('/users/:id', async (req, res) => {
  const user = await db.findUserById(Number(req.params.id));
  res.json(user);
});

app.post('/users', async (req, res) => {
  const newUser = await db.createUser(req.body);
  res.status(201).json(newUser);
});

app.put('/users/:id', async (req, res) => {
  const updated = await db.updateUser(Number(req.params.id), req.body);
  res.json(updated);
});

app.delete('/users/:id', async (req, res) => {
  await db.deleteUser(Number(req.params.id));
  res.status(204).end();
});
# --------------------------------------------------
# GraphQL (Apollo Server)
# --------------------------------------------------
type User {
  id: ID!
  name: String!
  email: String!
}

type Query {
  user(id: ID!): User
}

type Mutation {
  createUser(name: String!, email: String!): User
  updateUser(id: ID!, name: String, email: String): User
  deleteUser(id: ID!): Boolean
}

const resolvers = {
  Query: {
    user: (_, { id }) => db.findUserById(Number(id)),
  },
  Mutation: {
    createUser: (_, { name, email }) => db.createUser({ name, email }),
    updateUser: (_, { id, name, email }) =>
      db.updateUser(Number(id), { name, email }),
    deleteUser: (_, { id }) => db.deleteUser(Number(id)),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen({ port: 4000 });
// --------------------------------------------------
// gRPC (Protocol Buffers)
// --------------------------------------------------
syntax = "proto3";

package users;

service UserService {
  rpc GetUser(GetUserRequest) returns (UserResponse);
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
  rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}

message GetUserRequest { int64 id = 1; }
message UserResponse {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}
message CreateUserResponse { UserResponse user = 1; }

message UpdateUserRequest {
  int64 id = 1;
  string name = 2;
  string email = 3;
}
message UpdateUserResponse { UserResponse user = 1; }

message DeleteUserRequest { int64 id = 1; }
message DeleteUserResponse { bool success = 1; }
// gRPC server implementation (Node.js)
// --------------------------------------------------
import * as grpc from '@grpc/grpc-js';
import { UserServiceHandlers } from './proto/users_grpc_pb';
import { UserService } from './proto/users_pb';

const handlers: UserServiceHandlers = {
  getUser: async (call, callback) => {
    const user = await db.findUserById(call.request.getId());
    const response = new UserResponse();
    response.setId(user.id);
    response.setName(user.name);
    response.setEmail(user.email);
    callback(null, response);
  },
  // ... create, update, delete similar
};

const server = new grpc.Server();
server.addService(UserService, handlers);
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
  server.start();
});

Explanation

  • REST: Each CRUD operation maps to an HTTP verb and URL. The client can use any HTTP client (cURL, Axios, browser fetch).
  • GraphQL: All operations hit /graphql. The query/mutation shape dictates the response. Clients can request only the fields they need.
  • gRPC: Each method is a strongly‑typed RPC. The client must compile the .proto file to generate stubs, but the call is efficient and supports streaming if needed.

Common Mistakes & Trade‑offs (Expanded)

REST Over‑fetching

Mistake: Returning the entire user object when only the name and email are required.
Consequence: Increased payload, slower UI rendering.
Mitigation: Use selective field endpoints (GET /users/:id?fields=name,email) or switch to GraphQL for fine‑grained queries.

GraphQL Over‑engineering

Mistake: Designing a deeply nested schema to model every possible UI view.
Consequence: Resolver functions become complex, caching is harder, and performance degrades.
Mitigation: Keep the schema flat, use data loaders for batching, and document the public API surface.

gRPC Debugging Difficulty

Mistake: Logging raw protobuf messages in production.
Consequence: Logs are unreadable, making troubleshooting hard.
Mitigation: Use Envoy’s access logs, grpc‑web for browser debugging, and enable protocol‑buffer introspection in development.


Key Takeaways

  • REST is the safest bet for simple, public APIs where caching and HTTP tooling matter.
  • GraphQL offers the most client flexibility, at the cost of resolver complexity and potential over‑fetching if misused.
  • gRPC delivers the best performance for internal service meshes and real‑time workloads, but requires a stricter contract and more tooling for debugging.
  • Match the API style to your use case: CRUD‑heavy public services → REST; UI‑centric data fetching → GraphQL; high‑throughput micro‑services → gRPC.
  • Always consider future evolution: how will breaking changes be handled? Will the schema grow in a way that preserves backward compatibility?

Choosing the right API style isn’t a one‑size‑fits‑all decision; it’s a strategic trade‑off between performance, flexibility, and developer experience. Evaluate your application’s specific needs, and let the data shape, client requirements, and infrastructure guide your choice.