APIs and contracts

API design, a comparison of REST, GraphQL and gRPC, API versioning, and the contract-first approach.

Communication between components

In a modern architecture, components not only need to exist; they need to communicate in a clear, stable, and understandable way. That is where APIs and contracts come in.

Decisions about APIs directly affect the maintainability, scalability, and developer experience of the entire system.

What is an API?

An API is an interface that lets a system or component expose capabilities to others.

In simple terms, an API defines:

  • What you can request
  • How you should request it
  • What response you can expect
  • What errors may occur

In any distributed system, APIs are the communication mechanism between components. A well-designed API acts as a contract: it defines what a consumer can request, what it will receive, and under what conditions.

REST (Representational State Transfer)

REST is the most widely used architectural style for web APIs. It is based on resources identified by URLs and standard HTTP operations.

Core principles

  • Resources: Everything is a resource identified by a URI (/users/123).
  • HTTP verbs: GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove).
  • Stateless: Each request contains all the information needed; the server keeps no session state.
  • Representations: A resource can have multiple representations (JSON, XML).

Example of a REST API

GET    /api/orders          → List of orders
GET    /api/orders/42       → Detail of order 42
POST   /api/orders          → Create a new order
PATCH  /api/orders/42       → Update order 42
DELETE /api/orders/42       → Delete order 42

When it is useful

  • Simple or medium-sized APIs
  • Integration between services
  • Well-defined public or private interfaces
  • When you need native HTTP caching

Advantages of REST

  • Widely adopted and understood across the industry.
  • Cacheable natively via HTTP headers.
  • Mature tooling for documentation (OpenAPI/Swagger).
  • Works well with existing web infrastructure (proxies, CDNs, load balancers).

Disadvantages of REST

  • Over-fetching: The endpoint returns more data than the client needs.
  • Under-fetching: The client needs to make multiple requests to obtain related data.
  • No native typing; it relies on external documentation to define the structure.

GraphQL

GraphQL is a query language for APIs that lets the client specify exactly what data it needs.

How it works

# The client requests exactly what it needs
query {
  pedido(id: 42) {
    id
    fecha
    total
    cliente {
      nombre
      email
    }
    items {
      producto
      cantidad
    }
  }
}

Advantages of GraphQL

  • No over-fetching or under-fetching: The client defines the shape of the response.
  • A single endpoint: All queries go to /graphql.
  • Strong typing: The schema defines types, fields, and relationships explicitly.
  • Introspection: The client can discover the available schema at runtime.
  • Very well suited to BFFs for data aggregation.

Costs of GraphQL

  • Server-side complexity: Resolving nested queries can cause performance problems (N+1 queries).
  • Difficult caching: Since it does not use unique URLs per resource, standard HTTP caching does not apply directly.
  • Learning curve: It requires understanding a new paradigm on both the client and the server.
  • Security: Complex queries can be used for denial-of-service attacks if they are not limited.
  • More delicate performance control and observability that differs somewhat from REST.

gRPC

gRPC is a high-performance communication framework that uses Protocol Buffers (protobuf) for serialization and HTTP/2 as the transport.

Service definition

syntax = "proto3";

service ServicioPedidos {
  rpc ObtenerPedido (PedidoRequest) returns (PedidoResponse);
  rpc CrearPedido (CrearPedidoRequest) returns (PedidoResponse);
  rpc ListarPedidos (ListarRequest) returns (stream PedidoResponse);
}

message PedidoRequest {
  int32 id = 1;
}

message PedidoResponse {
  int32 id = 1;
  string fecha = 2;
  double total = 3;
}

Advantages of gRPC

  • High performance: Binary serialization (protobuf) is faster and more compact than JSON.
  • Bidirectional streaming: Supports real-time communication with HTTP/2.
  • Code generation: From the .proto file, clients and servers are generated in multiple languages.
  • Strict contracts: The protobuf schema is the contract; there is no ambiguity.

Disadvantages of gRPC

  • Does not work in browsers directly (it needs gRPC-Web as a proxy).
  • Hard to debug: Binary messages are not human-readable without special tools.
  • Lower adoption: Fewer tools and less documentation compared to REST.
  • Setup complexity: Requires a protobuf compiler and additional configuration.

Direct comparison

CriterionRESTGraphQLgRPC
Data formatJSON (text)JSON (text)Protobuf (binary)
TransportHTTP/1.1 or HTTP/2HTTP/1.1 or HTTP/2HTTP/2
TypingExternal (OpenAPI)Native (schema)Native (protobuf)
Ideal use casePublic APIs, CRUDFrontends with complex dataCommunication between services
PerformanceGoodVariableExcellent
CachingNative HTTPRequires a custom solutionNot standard
Learning curveLowMediumMedium-High

What is a contract

A contract is the agreement between producer and consumer. It can define:

  • Request and response structure
  • Required and optional fields
  • Data types
  • Possible errors
  • Required headers
  • Versioning rules

A contract reduces ambiguity and helps evolve systems without breaking consumers.

Contracts in events

APIs are not the only things that have contracts. Events also need clear schemas.

For example, if a service publishes OrderPlaced, other services need to know:

  • What fields it contains
  • What they mean
  • Which ones are required
  • How the schema evolves

Contract tools by technology

TechnologyContract tool
RESTOpenAPI (Swagger)
GraphQLSDL (Schema Definition Language)
gRPCProtocol Buffers (.proto)
EventsAsyncAPI

Contract-First design

The contract-first approach proposes defining the API before implementing it. Instead of writing code and generating documentation afterward, you define the contract and then implement against it.

Contract-first flow

1. Define the contract (OpenAPI, protobuf, GraphQL schema)

2. Review and validate with consumers

3. Generate the base code (server and client)

4. Implement the business logic

5. Validate that the implementation complies with the contract

Advantages of contract-first

  • Parallel development: Frontend and backend can work simultaneously using the contract as a reference.
  • Always up-to-date documentation: The contract is the documentation.
  • Automatic validation: Tools can verify that the implementation complies with the contract.
  • Better communication: The contract is a shared artifact between teams.

Evolution and versioning

Contracts change over time. That is why it is important to think about:

  • Backward compatibility: Changes should not break existing consumers.
  • Gradual deprecation: Mark endpoints or fields as deprecated and allow time to migrate.
  • Breaking changes: Identify when a change is incompatible and requires a new version.
  • Contract ownership: Define who is responsible for maintaining and evolving each contract.

Common versioning strategies

URL versioning

GET /api/v1/orders
GET /api/v2/orders

This is the simplest and most explicit strategy. The consumer knows exactly which version it is using.

Header versioning

GET /api/orders
Accept: application/vnd.myapi.v2+json

It keeps URLs clean but is less visible and harder to test in the browser.

Query parameter versioning

GET /api/orders?version=2

Simple, but it can interfere with caching and is not considered a good practice by many teams.

Versioning best practices

  • Avoid breaking changes: Add new fields instead of modifying existing ones.
  • Deprecate before removing: Mark endpoints as deprecated and give consumers time to migrate.
  • Document changes: Keep a clear changelog between versions.
  • Limit active versions: Do not keep more than 2-3 versions simultaneously.

Why contracts matter

Without clear contracts:

  • Teams interpret things differently
  • Fragile integrations appear
  • Changes break consumers
  • Accidental coupling grows

With well-defined contracts:

  • There is less ambiguity
  • Evolution becomes safer
  • Teams can coordinate better

Principles of good API design

Regardless of the technology chosen, these practices apply to any API:

  • Consistency: Use uniform conventions in names, formats, and error codes.
  • Idempotency: GET, PUT, and DELETE operations should produce the same result if executed multiple times.
  • Clear error handling: Return appropriate status codes and helpful error messages.
  • Pagination: Never return full collections without a limit; implement pagination from the start.
  • Security: Authentication (who you are) and authorization (what you can do) on every endpoint.

Which one to choose?

  • REST if you need a public, simple, and widely compatible API.
  • GraphQL if your frontend needs the flexibility to query complex, related data.
  • gRPC if you need high-performance communication between internal services.

In many real systems, all three technologies coexist: REST for public APIs, GraphQL for the BFF that feeds the frontend, and gRPC for internal communication between microservices.

Summary

APIs enable communication between components. Contracts make that communication explicit, maintainable, and evolvable. In distributed systems, thinking through contracts carefully is just as important as writing the code that implements them.