Full Stack • Java • System Design • Cloud • AI Engineering

API Types

A comprehensive guide to API types — covering REST, RPC/gRPC, GraphQL, event-driven, SOAP, WebSocket, and webhook APIs. Learn what each type solves, how it works, when to choose it, and how to recognise which type a real-world integration requires.

API Types

Not all APIs look like GET /orders. The choice of API type is one of the most consequential decisions in system design — it determines the communication model, latency characteristics, contract shape, tooling ecosystem, and how both producers and consumers must be built.

This article covers the seven primary API types used in production systems:

Type Communication Model Protocol Primary Use Case
REST Request / Response HTTP/1.1, HTTP/2 Web, mobile, public APIs, CRUD
RPC / gRPC Request / Response HTTP/2 (gRPC) Service-to-service, streaming, low latency
GraphQL Request / Response HTTP Flexible querying, multi-client, BFF
Event-Driven Publish / Subscribe Kafka, AMQP, SNS Async workflows, high throughput, decoupling
WebSocket Bidirectional persistent HTTP upgrade + WS Real-time: chat, feeds, live dashboards
Webhook Server push (HTTP POST) HTTP Notifications, integrations, event delivery
SOAP Request / Response HTTP, SMTP, JMS Enterprise, regulated, legacy integrations

REST (Representational State Transfer)

REST is an architectural style — not a protocol — that uses HTTP as a transport and models everything as resources that can be created, read, updated, or deleted.

How It Works

A REST API exposes resources at URL paths. The HTTP method encodes the intent:

GET    /v1/accounts/{id}          → read an account
POST   /v1/accounts               → create an account
PUT    /v1/accounts/{id}          → replace an account
PATCH  /v1/accounts/{id}          → partially update an account
DELETE /v1/accounts/{id}          → delete an account

Requests and responses are typically JSON over HTTPS. The server is stateless — each request contains all the information needed to process it; the server holds no session state between calls.

Example Interaction

Request:

GET /v1/accounts/acc_7f3a9b2c HTTP/1.1
Host: api.bank.com
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Accept: application/json

Response:

{
  "accountId": "acc_7f3a9b2c",
  "type": "CHECKING",
  "status": "ACTIVE",
  "currency": "USD",
  "availableBalance": 4250.00,
  "owner": {
    "customerId": "cust_a1b2c3",
    "name": "Jane Smith"
  },
  "createdAt": "2024-03-15T09:00:00Z"
}

REST Constraints

A true REST API satisfies six architectural constraints:

  1. Client-server — UI and data storage are separated
  2. Stateless — no client session state on the server
  3. Cacheable — responses declare whether they can be cached
  4. Uniform interface — consistent resource addressing, self-descriptive messages
  5. Layered system — client cannot tell if it's talking to the origin server or a proxy
  6. Code on demand (optional) — servers can extend client functionality by sending executable code

Most "REST APIs" in practice satisfy the first four. The uniform interface is the most important in practice.

When to Use REST

✅ Public APIs exposed to external developers
✅ Mobile and web client-to-server communication
✅ CRUD-heavy services with clear resource models
✅ Integrations where consumers are unknown and diverse
✅ Services where HTTP caching provides value
✅ Any API that must be human-navigable through a browser

When REST Is Not the Right Choice

❌ Real-time bidirectional communication (use WebSocket)
❌ Service-to-service calls where latency < 10 ms matters (consider gRPC)
❌ Clients that need exactly the fields they specify — no more (consider GraphQL)
❌ High-throughput event streaming (use an event-driven approach)


RPC / gRPC (Remote Procedure Call)

RPC models API interactions as function calls rather than resource operations. Instead of GET /orders/123, you call GetOrder(orderId: "123"). The most widely used modern RPC protocol is gRPC, developed by Google.

How gRPC Works

gRPC uses three technologies together:

  1. Protocol Buffers (protobuf) — a language-neutral binary serialization format that defines the service contract in .proto files
  2. HTTP/2 — the transport, which supports multiplexing, header compression, and bidirectional streaming
  3. Code generation — the .proto definition generates client and server stubs in any supported language

Service definition (.proto file):

syntax = "proto3";

package banking.v1;

service AccountService {
  rpc GetAccount (GetAccountRequest) returns (Account);
  rpc CreateAccount (CreateAccountRequest) returns (Account);
  rpc ListAccounts (ListAccountsRequest) returns (ListAccountsResponse);
  rpc StreamTransactions (StreamTransactionsRequest) returns (stream Transaction);
}

message GetAccountRequest {
  string account_id = 1;
}

message Account {
  string account_id = 1;
  string type = 2;
  string status = 3;
  string currency = 4;
  double available_balance = 5;
  google.protobuf.Timestamp created_at = 6;
}

The Four gRPC Patterns

gRPC supports four interaction models that REST cannot natively provide:

1. Unary             → one request, one response (like REST)
   rpc GetAccount(Request) returns (Account)

2. Server streaming  → one request, stream of responses
   rpc StreamTransactions(Request) returns (stream Transaction)

3. Client streaming  → stream of requests, one response
   rpc BatchUpload(stream DataRecord) returns (UploadResult)

4. Bidirectional     → stream of requests and stream of responses simultaneously
   rpc Chat(stream Message) returns (stream Message)

Why gRPC Is Faster Than REST

Factor REST (JSON/HTTP 1.1) gRPC (protobuf/HTTP/2)
Serialization JSON text (verbose) Protobuf binary (~3–5× smaller)
Multiplexing One request per connection Multiple concurrent streams per conn
Header compression None HPACK compression
Type safety Runtime only Compile-time from .proto

In benchmarks, gRPC typically achieves 5–10× higher throughput and 50–70% lower latency than equivalent REST+JSON over the same infrastructure.

When to Use gRPC

✅ Service-to-service communication inside a microservices platform
✅ Low-latency, high-throughput internal APIs
✅ Streaming scenarios (server push, bidirectional streams)
✅ When strong, compile-time type safety is required
✅ Polyglot environments (Java service ↔ Go service ↔ Python service)

When gRPC Is Not the Right Choice

❌ Browser-native clients (browsers do not support gRPC directly; use gRPC-Web or a REST gateway)
❌ Public developer APIs (protobuf is less approachable than JSON for external developers)
❌ Systems where human-readable wire format is needed for debugging
❌ Environments that block HTTP/2 (some enterprise proxies)


GraphQL

GraphQL is a query language for APIs developed by Facebook (Meta) in 2012 and open-sourced in 2015. Instead of a collection of fixed endpoints, a GraphQL API exposes a single endpoint where the client sends a query describing exactly the data it needs.

How GraphQL Works

The server defines a schema — the types, relationships, and operations available:

type Query {
  order(id: ID!): Order
  orders(filter: OrderFilter, first: Int, after: String): OrderConnection
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order
  cancelOrder(id: ID!): Order
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order
}

type Order {
  id: ID!
  status: OrderStatus!
  customer: Customer!
  items: [OrderItem!]!
  totalAmount: Money!
  createdAt: DateTime!
}

The client sends a query specifying exactly which fields to return:

query {
  order(id: "ord_abc123") {
    id
    status
    customer {
      name
      email
    }
    items {
      productName
      quantity
      unitPrice
    }
  }
}

The response contains exactly the fields requested — nothing more:

{
  "data": {
    "order": {
      "id": "ord_abc123",
      "status": "CONFIRMED",
      "customer": {
        "name": "Jane Smith",
        "email": "[email protected]"
      },
      "items": [
        { "productName": "Laptop Stand", "quantity": 1, "unitPrice": 49.99 }
      ]
    }
  }
}

Solving Over-fetching and Under-fetching

REST APIs have a fundamental problem: the endpoint defines the shape of the response, not the consumer.

Over-fetching: The response contains fields the client doesn't need.

// Client only needs name and email, but gets all of this:
{
  "id": "cust_abc",
  "name": "Jane",
  "email": "[email protected]",
  "phone": "+1-555-0100",
  "addressLine1": "123 Main St",
  "addressLine2": null,
  "city": "Austin",
  "state": "TX",
  "zipCode": "78701",
  "country": "US",
  "dateOfBirth": "1985-04-12",
  "createdAt": "2023-01-15T09:00:00Z"
}

Under-fetching: One endpoint doesn't return all the data needed, requiring multiple sequential calls.

GET /v1/orders/ord_abc123       → get order (no customer details)
GET /v1/customers/cust_abc       → get customer (separate call)
GET /v1/products/prod_xyz        → get product details (separate call)

GraphQL solves both with a single query that fetches exactly the required data across all related resources in one round trip.

GraphQL Operations

Operation Purpose Analogy
query Read data (side-effect free) GET
mutation Create, update, or delete data POST/PUT/DELETE
subscription Real-time event stream over WebSocket WebSocket/SSE

When to Use GraphQL

✅ Multiple client types (web, mobile, smart TV) needing different data shapes
✅ Rapidly evolving frontends that need flexibility over the backend schema
✅ Backend-for-Frontend (BFF) pattern — GraphQL as the aggregation layer
✅ Applications with complex, deeply nested relational data
✅ When reducing the number of API round trips is a priority

When GraphQL Is Not the Right Choice

❌ Simple CRUD services with uniform data needs
❌ Public APIs where HTTP caching at the CDN level is important (GraphQL POSTs are not cacheable by default)
❌ Teams without discipline around query depth limits, rate limiting per query cost, and the N+1 problem
❌ High-throughput batch processing where overhead of query parsing matters


Event-Driven APIs (Async APIs)

Event-driven APIs do not follow the request-response model. Instead of a caller asking for something and waiting for an answer, a producer publishes an event and consumers react to it asynchronously.

How Event-Driven APIs Work

flowchart LR
    PRODUCER[Order Service] -->|publishes event| BROKER[(Message Broker\nKafka / RabbitMQ)]
    BROKER -->|delivers event| C1[Fulfillment Service]
    BROKER -->|delivers event| C2[Notification Service]
    BROKER -->|delivers event| C3[Analytics Service]

The producer publishes a message to a topic or queue. Any number of consumers can subscribe independently — the producer never calls them directly and does not wait for them.

Published event (Kafka message):

{
  "eventId": "evt_d4e5f6a7",
  "eventType": "order.confirmed",
  "schemaVersion": "1.2.0",
  "aggregateId": "ord_abc123",
  "producedAt": "2026-07-20T10:30:00Z",
  "payload": {
    "orderId": "ord_abc123",
    "customerId": "cust_xyz789",
    "totalAmountCents": 4999,
    "currency": "USD",
    "items": [
      { "sku": "LAPTOP-STAND-01", "quantity": 1, "unitPriceCents": 4999 }
    ]
  }
}

Message Brokers

Broker Model Ordering Replay Use Case
Kafka Log / pub-sub Per partition ✅ Yes High throughput, event sourcing
RabbitMQ Queue / pub-sub Per queue ❌ No Task queues, routing by message type
Amazon SQS Queue FIFO option ❌ No Managed task queue, serverless
Amazon SNS Fan-out pub-sub None ❌ No One-to-many notifications
Google Pub/Sub Pub-sub None ✅ Limited GCP event streaming

Event-Driven Patterns

Pub/Sub (Fan-out): One producer, many consumers. All consumers receive every message.

Queue (Work queue): One producer, many competing consumers. Each message is processed by exactly one consumer (for task distribution and load balancing).

Event Sourcing: The event log is the system of record. Current state is derived by replaying events from the beginning. Every state change is captured as an immutable event.

CQRS (Command Query Responsibility Segregation): Write operations (commands) publish events. Read operations (queries) use projections built from those events.

When to Use Event-Driven APIs

✅ Multiple services need to react to the same state change
✅ The producer should not wait for consumers (temporal decoupling)
✅ High-throughput pipelines (millions of events per second)
✅ Audit logs and event sourcing
✅ Workflows that span multiple services (saga pattern)
✅ Streaming analytics and real-time data processing

When Event-Driven APIs Are Not the Right Choice

❌ When the caller needs a synchronous answer before proceeding
❌ Simple CRUD operations with low volume
❌ When message ordering is critical across multiple topics (Kafka only guarantees ordering within a partition)
❌ Teams without the operational maturity to manage message brokers, DLQs, and consumer lag


WebSocket

WebSocket is a protocol that provides a persistent, full-duplex (bidirectional) communication channel over a single TCP connection. Unlike HTTP, which is request-response, a WebSocket connection stays open — either side can send a message to the other at any time without a new request.

How WebSocket Works

A WebSocket connection starts as an HTTP request and is upgraded to a WebSocket connection:

Client → Server:
GET /chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Server → Client:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After the handshake, the connection is persistent. Both sides can send frames at any time:

Client → Server: { "type": "message", "content": "Hello team!" }
Server → Client: { "type": "message", "from": "alice", "content": "Welcome!" }
Server → Client: { "type": "typing", "user": "bob" }
Client → Server: { "type": "ping" }
Server → Client: { "type": "pong" }

WebSocket vs HTTP Polling

Without WebSocket, clients wanting real-time updates must poll:

Client → Server: GET /notifications (every 2 seconds)
Server → Client: [] (empty — no new notifications)
Client → Server: GET /notifications (2 seconds later)
Server → Client: [] (still empty)
... (100 requests before a notification arrives)
Client → Server: GET /notifications
Server → Client: [{ "type": "order_confirmed", "orderId": "ord_abc123" }]

Every poll is a new HTTP connection. Most polls return empty. This wastes bandwidth, server resources, and battery on mobile.

With WebSocket, the server pushes the notification the instant it occurs — one persistent connection, zero wasted requests.

When to Use WebSocket

✅ Real-time chat applications
✅ Live dashboards (stock prices, monitoring metrics, sports scores)
✅ Multiplayer games
✅ Collaborative editing (Google Docs-style)
✅ Live order tracking, delivery tracking
✅ Notifications that must arrive within milliseconds

When WebSocket Is Not the Right Choice

❌ Data that updates infrequently (once a minute or less) — polling or SSE is simpler
❌ One-directional server push — Server-Sent Events (SSE) is a lighter alternative
❌ Stateless, request-response interactions — HTTP does this better
❌ Environments that terminate long-lived connections (some corporate proxies, load balancers with short idle timeouts)

Server-Sent Events (SSE) — A Simpler Alternative

For server-to-client push (one direction only), SSE is often the right choice over WebSocket:

GET /v1/notifications HTTP/1.1
Accept: text/event-stream

data: {"type":"order_confirmed","orderId":"ord_abc123"}\n\n
data: {"type":"payment_processed","paymentId":"pmt_xyz789"}\n\n

SSE uses plain HTTP, works over HTTP/2, handles reconnections automatically, and requires no WebSocket upgrade. Use it when you only need server-to-client push and bidirectional communication is not required.


Webhook

A webhook is an HTTP callback — instead of the client polling the server for updates, the server sends an HTTP POST to a URL the client has registered whenever an event occurs.

How Webhooks Work

sequenceDiagram
    participant APP as Your Application
    participant STRIPE as Stripe API
    participant WH as Your Webhook Endpoint

    APP->>STRIPE: POST /v1/charges (create a charge)
    STRIPE-->>APP: { "chargeId": "ch_abc", "status": "pending" }

    note over STRIPE: Payment is processed asynchronously

    STRIPE->>WH: POST /webhooks/stripe
    note right of STRIPE: HTTP POST with event payload
    WH->>WH: Verify signature, process event
    WH-->>STRIPE: 200 OK

Webhook payload delivered by Stripe:

{
  "id": "evt_1NkLHb2eZvKYlo2CmSp2XXXX",
  "object": "event",
  "type": "payment_intent.succeeded",
  "created": 1690000000,
  "data": {
    "object": {
      "id": "pi_3NkLHb2eZvKYlo2C",
      "amount": 4999,
      "currency": "usd",
      "status": "succeeded",
      "metadata": {
        "orderId": "ord_abc123"
      }
    }
  }
}

Webhook Security: Signature Verification

Webhooks are incoming HTTP requests from the internet — anyone could POST to your webhook endpoint. Always verify the signature:

import hmac
import hashlib

def verify_stripe_webhook(payload: bytes, sig_header: str, secret: str) -> bool:
    timestamp, signature = parse_stripe_signature(sig_header)
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{payload.decode()}".encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Webhook Reliability Patterns

Webhooks can fail — your endpoint might be down, slow, or return a non-2xx response. Production-quality webhook consumers must:

  1. Return 200 immediately — acknowledge receipt before doing any processing to avoid timeout retries
  2. Process asynchronously — put the event on an internal queue and return 200 fast
  3. Handle retries idempotently — Stripe retries failed deliveries; use the event.id to deduplicate
  4. Monitor DLQ — if all retries fail, the event goes to a dead-letter queue; alert on it
@app.route("/webhooks/stripe", methods=["POST"])
def stripe_webhook():
    payload = request.get_data()
    sig = request.headers.get("Stripe-Signature")

    if not verify_stripe_webhook(payload, sig, STRIPE_WEBHOOK_SECRET):
        return "Unauthorized", 401

    event = json.loads(payload)

    # Idempotency check
    if event_already_processed(event["id"]):
        return "OK", 200

    # Enqueue for async processing
    queue.enqueue(process_stripe_event, event)

    return "OK", 200  # Always return 200 quickly

When to Use Webhooks

✅ Receiving notifications from third-party services (Stripe payments, GitHub pushes, Twilio SMS delivery)
✅ Integrations where the event source is external and cannot connect to your message broker
✅ B2B event delivery to partner systems
✅ Replacing polling for infrequent but important events

Webhook Limitations

❌ The consumer must have a publicly accessible HTTPS endpoint
❌ No built-in ordering guarantees — events can arrive out of order
❌ No native replay — if your endpoint was down during delivery, you may miss events
❌ Delivery guarantees vary by provider — always check the provider's retry policy


SOAP (Simple Object Access Protocol)

SOAP is an XML-based messaging protocol standardised by W3C. It defines a formal envelope structure, uses WSDL (Web Services Description Language) for machine-readable contracts, and supports a rich ecosystem of WS-* standards for security, reliability, and transactions.

How SOAP Works

Every SOAP message has the same structure:

<soap:Envelope
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:tns="https://api.bank.com/accounts">

  <soap:Header>
    <wsse:Security xmlns:wsse="...">
      <wsse:UsernameToken>
        <wsse:Username>system-user</wsse:Username>
        <wsse:Password Type="...PasswordText">secret</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
  </soap:Header>

  <soap:Body>
    <tns:GetAccountBalanceRequest>
      <tns:AccountId>ACC-7F3A9B2C</tns:AccountId>
      <tns:AsOfDate>2026-07-20</tns:AsOfDate>
    </tns:GetAccountBalanceRequest>
  </soap:Body>

</soap:Envelope>

SOAP response:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <tns:GetAccountBalanceResponse>
      <tns:AccountId>ACC-7F3A9B2C</tns:AccountId>
      <tns:Currency>USD</tns:Currency>
      <tns:AvailableBalance>4250.00</tns:AvailableBalance>
      <tns:AsOf>2026-07-20T09:00:00Z</tns:AsOf>
    </tns:GetAccountBalanceResponse>
  </soap:Body>
</soap:Envelope>

WSDL — The Machine-Readable Contract

SOAP services are described by a WSDL (Web Services Description Language) file that fully specifies every operation, input/output message, data type, and binding. WSDL allows code generation tools to produce strongly typed client stubs in any language — similar to what protobuf does for gRPC.

WS-* Standards

SOAP's extensibility through SOAP headers supports a range of enterprise standards:

Standard Purpose
WS-Security Message-level encryption, signing, token-based auth
WS-ReliableMsg Guaranteed delivery with acknowledgement
WS-AtomicTx Distributed ACID transactions across multiple services
WS-Addressing Message routing independent of transport

These capabilities are native to SOAP and do not exist in REST. For regulated industries where end-to-end message signing, non-repudiation, and guaranteed delivery are legal requirements, SOAP remains the correct choice.

When to Use SOAP

✅ Legacy enterprise integration where SOAP endpoints are the only option
✅ Financial services integrations requiring WS-Security message signing
✅ Healthcare HL7 / FHIR integrations on older infrastructure
✅ Government system integrations
✅ ACID transactions spanning multiple systems
✅ Non-repudiation requirements (proof that a specific message was sent)

When SOAP Is Not the Right Choice

❌ New greenfield services — use REST or gRPC
❌ Mobile or browser clients — XML verbosity is costly on constrained devices
❌ Teams without SOAP tooling experience
❌ Public developer APIs where onboarding friction matters


Choosing the Right API Type

The decision is driven by the interaction model required, not by preference.

flowchart TD
    A{What does the caller need?}

    A -->|"Synchronous answer now"| B{Who is the caller?}
    A -->|"React to something that happened"| C{Volume / frequency?}
    A -->|"Push data as it becomes available"| D{Direction?}

    B -->|"External developer / browser"| E[REST]
    B -->|"Internal service, need speed"| F[gRPC]
    B -->|"Frontend needing flexible data"| G[GraphQL]
    B -->|"Enterprise / legacy system"| H[SOAP]

    C -->|"High throughput / decouple services"| I[Event-Driven / Kafka]
    C -->|"Third-party notification / low volume"| J[Webhook]

    D -->|"Server → Client only"| K[SSE or Webhook]
    D -->|"Both directions / real-time"| L[WebSocket]

Decision Guide

Scenario API Type
Public REST API for external developers REST
Internal microservice calls in a distributed system gRPC
Mobile app that needs different data than the web app GraphQL
Payment confirmation from Stripe to your backend Webhook
Order service notifying fulfillment + notification services Event-Driven (Kafka)
Live chat application WebSocket
Real-time stock price feed to a browser WebSocket or SSE
Bank-to-bank SWIFT integration SOAP
Order status feed (server push only) SSE
Background job worker queue Event-Driven (SQS/RabbitMQ)

Mixing API Types in One System

Real systems use multiple API types simultaneously. A mature e-commerce platform might use:

  • REST — public API for partner integrations and mobile clients
  • gRPC — service-to-service calls between checkout, inventory, and pricing services
  • GraphQL — backend-for-frontend aggregation layer for the web storefront
  • Kafka — order confirmed → fulfillment, notification, analytics pipeline
  • WebSocket — live order tracking updates pushed to the customer's browser
  • Webhooks — payment processor delivers payment confirmation events
  • SOAP — integration with a legacy warehouse management system

No single API type is the right answer for everything. The skill is matching the type to the use case.


Summary

flowchart LR
    A[REST] -->|"Resources, HTTP, JSON"| Z[Choose based on interaction model]
    B[gRPC] -->|"Procedure calls, binary, streaming"| Z
    C[GraphQL] -->|"Flexible queries, client-driven"| Z
    D[Event-Driven] -->|"Publish-subscribe, async"| Z
    E[WebSocket] -->|"Bidirectional, real-time"| Z
    F[Webhook] -->|"Server push via HTTP callback"| Z
    G[SOAP] -->|"XML, enterprise, WS-* standards"| Z

Key takeaways:

  • REST is the default for web and mobile APIs — stateless, HTTP-native, widely understood
  • gRPC is the default for internal service-to-service calls — faster, streaming-capable, strongly typed
  • GraphQL solves over-fetching and under-fetching when multiple client types need different data shapes
  • Event-driven APIs decouple producers from consumers — use when asynchronous reaction and high throughput matter
  • WebSocket provides real-time bidirectional communication — use for chat, live dashboards, collaborative apps
  • Webhooks are server-to-client HTTP callbacks — the standard way third-party services deliver event notifications
  • SOAP is the correct choice for regulated enterprise integrations requiring message-level security, signed envelopes, and guaranteed delivery
  • Most production systems use several types simultaneously — the skill is choosing the right type for each integration boundary

Learning Path

This page is part of the API Engineering Fundamentals Learning Path.

Return to the API Engineering Learning Path to continue with the complete roadmap.