Modern applications need to handle different types of communication. A mobile app requests user profile data. One backend service fetches information from another. A payment provider notifies your server when a transaction succeeds. A chat application delivers new messages to everyone in a conversation.
Each of these scenarios has different requirements, and that's why we have different API styles.
In this chapter, we will look at the main API styles, understand how each one works, and see where each fits in a real system.
An API defines how two pieces of software communicate. The API style defines the pattern that communication follows.
An API style answers questions like these:
Different API styles answer these questions in different ways.
Let's start with the most widely used one: REST.
REST stands for Representational State Transfer. In REST, instead of thinking in terms of functions or actions, we think in terms of resources.
For example, users, orders, and products can all be treated as resources. Each resource is identified by a URL. We then use standard HTTP methods to describe what we want to do with those resources.
The method carries the meaning of the action:
| Method | Common Meaning | Safe | Idempotent |
|---|---|---|---|
| GET | Read a resource | Yes | Yes |
| POST | Create or trigger processing | No | No by default |
| PUT | Replace a resource | No | Yes |
| PATCH | Partially update a resource | No | Depends on the patch |
| DELETE | Delete a resource | No | Yes |
"Safe" means the request does not change anything on the server. "Idempotent" means sending the same request twice has the same effect as sending it once.
If the client sends GET /users/123, the server might respond with a JSON object containing the user's ID, name, and email.
REST APIs are also typically stateless. This means every request should contain all the information the server needs to process it, such as the auth token in the example above. The server should not rely on context stored from a previous request.
This makes REST services easier to scale, because any server behind a load balancer can handle any request.
One of the main reasons REST became so popular is that it maps directly onto HTTP. It is relatively simple to understand, easy to debug, supported almost everywhere, and works well for public APIs and traditional client-server applications. HTTP status codes, cache headers, and common tooling all work with it out of the box.
Strict REST vs practical REST: Roy Fielding's original definition of REST also includes HATEOAS, where each response contains links that tell the client which actions are available next. Most APIs called "RESTful" skip this. They use resource URLs, HTTP methods, and stateless requests, and describe the API with documentation such as OpenAPI instead.
But REST has limitations.
For example, a profile page might need three requests:
Each request is a separate round trip, which adds latency, especially on mobile networks. This is one of the problems that GraphQL was designed to address.
GraphQL takes a different approach. Instead of the server deciding the exact shape of each response, the client specifies exactly what data it needs.
Suppose a profile page needs a user's name, profile picture, and five recent orders. With REST, you might need multiple requests: one for the user, another for their profile, and another for their orders, with a limit of five.
With GraphQL, the client can ask for all of that in a single query:
The query selects user 123, requests their name and profile picture, and asks for the id and total of five orders. The server returns only the fields that were requested, in the same shape as the query:
GraphQL APIs are built around a strongly defined schema that describes the available types, fields, and operations. Clients can use that schema to discover what data they can request.
Besides queries for reads, GraphQL has mutations for writes and subscriptions for receiving live updates.
This makes GraphQL especially useful when different clients need different views of the same data. For example, a mobile app may need a lightweight response, while a desktop application may need much more information. Both can query the same GraphQL API but request different fields.
That flexibility also introduces additional complexity.
friends { friends { friends { orders } } }. Teams usually add limits on query depth or cost to prevent overly expensive queries.So GraphQL works especially well when client flexibility is important and the data relationships are complex. When a small set of stable REST endpoints covers the use case, REST is usually easier to secure, cache, and operate.
Like REST, GraphQL typically sends text-based JSON, and the client initiates each request. That works well for many client-facing APIs. But for high-throughput communication between dozens or hundreds of internal microservices, teams often want something more efficient. This is where RPC-style APIs are commonly used.
RPC stands for Remote Procedure Call. The idea is to make calling a function on another server look like calling a local function in your own code.
Instead of being organized around resources, RPC is organized around actions. For example, with REST you might write POST /orders. With RPC, the API might look more like:
This changes how the API is designed. With REST, the question is, "What resource do I want to work with?" With RPC, it is, "What operation do I want the server to perform?"
In practice, the client sends the function name and its parameters to the remote server. The server executes that operation and sends the result back.
RPC is also not tied to one specific protocol or data format. Different RPC frameworks use different transports and serialization formats. JSON-RPC, for example, sends calls as JSON over HTTP. One of the most popular modern RPC frameworks is gRPC.
gRPC is a modern RPC framework designed for fast and efficient communication between services.
Instead of sending human-readable JSON like REST APIs often do, gRPC commonly uses Protocol Buffers, or Protobuf, to define the API and serialize the data.
For example, we might define a UserService with a GetUser operation that accepts a GetUserRequest and returns a GetUserResponse:
This acts as a contract between the client and the server. It defines what operations are available, what data the client must send, and what response it should expect.
From this definition, gRPC can automatically generate client and server code in multiple programming languages. So a Java service, for example, can communicate with a Go or Python service using the same API contract.
Another major difference is that Protobuf uses a compact binary format instead of text-based JSON. The same user looks like this in each format:
Protobuf does not repeat the field names in every message. Both sides already know them from the contract, so only the field numbers and values are sent. That usually means smaller payloads and faster serialization, which makes gRPC especially useful for high-volume internal communication.
gRPC also runs on HTTP/2, which gives it features like multiplexing (many calls share one connection at the same time) and streaming (either side can send a stream of messages). It also supports deadlines, so a caller can say how long it is willing to wait.
Because of these features, gRPC is commonly used for communication between microservices.
But gRPC also has trade-offs:
So a common pattern is to use REST or GraphQL for external clients, while using gRPC for communication between internal services.
Next, let's move away from request-response APIs entirely and look at WebSockets.
WebSockets are useful when the client and server need to communicate continuously in real time.
With a normal HTTP API, the client sends a request, the server sends back a response, and that interaction is finished. WebSockets work differently. The client first opens a persistent connection with the server. Once that connection is established, both the client and the server can send messages whenever they need to.
The connection starts as a normal HTTP request that asks to upgrade. The server agrees with a 101 Switching Protocols response, and from then on both sides send WebSocket messages over the same connection.
This makes WebSockets especially useful when the server needs to push updates immediately, without waiting for the client to make another request. That is why they are commonly used for chat applications, live notifications, multiplayer games, and real-time dashboards.
The trade-off is that WebSockets make the backend more complex. The server now has to:
That last point gets harder at scale. If Alice is connected to server A and Bob to server B, a message from Alice has to find its way to server B. Systems usually add a shared pub/sub layer, such as Redis, to route messages between servers. Load balancers also need to support long-lived connections.
So WebSockets are a good fit when you need low-latency, bidirectional communication. But if your application mostly follows a simple request-response pattern, a regular HTTP API is usually simpler to build, scale, and operate.
The API styles we have seen so far usually require the consumer to initiate the request. That works well when a browser, mobile app, or another service knows when it needs data.
But sometimes the event happens somewhere else, and you want to be notified automatically.
For example, suppose Stripe needs to tell your server that a payment has just succeeded. Your server should not have to poll Stripe repeatedly to check whether the payment has succeeded. Instead, Stripe can send an HTTP request to your server as soon as the event happens. This is called a webhook.
Webhooks are useful when one system needs to notify another system that something has happened. Instead of the receiving system polling repeatedly for changes, it gives the sender a URL to call whenever an event occurs.
This pattern is common for payment events, GitHub repository updates, email delivery notifications, CI/CD pipelines, and third-party integrations.
A webhook request is just an HTTP POST with a JSON body describing the event:
Webhooks are usually asynchronous. The system sending the event does not need to wait for the receiving application to finish all of its processing. It sends the notification and continues with other work. On the receiving side, a common approach is to store the event, return 200 OK quickly, and do the real work in a background job.
There are a few important things to get right:
Stripe-Signature header is an HMAC of the body computed with a secret only you and Stripe know.The five styles above cover most of what you will design. A few others show up often enough to know about.
Server-Sent Events let a server send a one-way stream of updates to the browser over a normal HTTP connection. The browser uses the built-in EventSource API.
SSE is simpler than WebSockets when only the server needs to push updates, such as live notifications, progress bars, or streaming tokens from an LLM. If the client also needs to send frequent messages back, use WebSockets.
Some services do not call each other directly at all. One service publishes an event to a message broker such as Kafka, and other services consume it independently.
This fits workflows where the producer should not wait for every consumer, such as order events, notifications, and audit logs. The trade-off is that debugging, ordering, and schema changes get harder.
SOAP is an older XML-based protocol with formal contracts defined in WSDL files. It is rare in new APIs, but you will still find it in banking, insurance, healthcare, government, and older enterprise integrations. Use it when an integration partner requires it.
In real systems, these API styles are not mutually exclusive. Most systems use several of them together.
Imagine an e-commerce application:
So each API style solves a different communication problem. That is why, when designing a system, the question usually should not be, "Which API style should the entire system use?" A better question is, "Which API style is the best fit for each communication path?"
| Communication path | Good fit | Avoid when |
|---|---|---|
| Public API working with resources | REST | Clients need very different nested views of the data |
| Client screens with varied data needs | GraphQL | A few stable endpoints cover the use case |
| Internal service-to-service calls | gRPC | Browsers need to call it directly without a proxy |
| Two-way real-time interaction | WebSocket | Normal request-response is enough |
| One-way live updates to a browser | SSE | The client also sends frequent messages |
| Another system notifies you of an event | Webhook | The caller needs an immediate result from your system |
| Many services react to the same event | Message-driven API | The caller needs a response from every consumer |
An API style defines the pattern two pieces of software follow when they communicate.
Most real systems use several of these together. Pick the style that fits each communication path, not one style for the whole system.
In the rest of this section, we will take a closer look at each one.
10 quizzes