API Design

API Types

Pasted image 20260901202615.png

API TypeDescriptionUse when
RESTUse standard HTTP methodDefautl go to, when need to expose to client
GraphQLSingle endpoint with query language to route differently- Web dashboard when we have lots of different request
- When we need "flexible data fetching"
- When we need to avoid over-fetching and under-fetching
RPCRPC protocol like gRPC, use HTTP/2. RPC you use function call like checkPermission(userId, resources) that would sounds more naturally. Very high performance however not widely adopted by different clients. Use for internal only- Internal service-to-service call when performance is critical

REST

Resources modelling

All the REST resources need to be plural nouns, NOT VERBS. Instead of book or purchase, we need to do (events, bookings, tickets)

If the 2 resources have a relationship, we should nest the resources: i.e events → tickets: /events/{id}/tickets

Otherwise, we can just use /tickets?event_id={event_id}?section=VIP. This option works better in a search operation due to its flexibility.

HTTP methods

  1. GET: retrieve only, no change
  2. POST: create new resources, NOT safe and NOT Idempotent — Calling multiple time create multiple resources
  3. PUT: replace entire resource, however PUT IS Idempotent
  4. PATCH: Patch update part of the result. PATCH is not guarantee to be Idempotent.
    1. PATCH to update an email field is idempotent
    2. PATCH to append to a list is not idempotent
  5. DELETE: remove the entire resource

Passing data to apis

  1. Path parameter: /events/123 — use when the value is required to identify the resource
  2. Query parameter: /eents?city=NYC&date=2024-01-01&... — use when search, filter, sort
    1. These goes with ? and then follow by &
  3. Request body: use to update/create a resource

GraphQL

See: GraphQL Query

Good:

  1. when different client may need different data than the api provided. Avoid "over-fetching" and "under-fetching".
  2. when frontend teams need to iterate quickly without backend changes. With REST — backend team need to add new endpoints etc. With GraphQL frontend can request additional fields as long as they exists in the schema.
    Bad:
  3. Complexity: need to implement schema validation, parsing
  4. Sophisticated caching strategies
  5. Authentication is per field rather than entire endpoint

Schema design

You would need to design your schema something like this

type Event {
  id: ID!
  name: String!
  date: DateTime!
  venue: Venue!
  tickets: [Ticket!]!
}

type Venue {
  id: ID!
  name: String!
  address: String!
}

type Query {
  event(id: ID!): Event
  events(limit: Int, after: String): [Event!]!
}

But this create N+1 problems. Imagine you need to query for 100 events, you do

  1. Query 100 events using 1 query getAll()
  2. For each event, need to do 1 query each to get the venue

So we did 101 queries instead of just 2 (1 for events and 1 for all events in each id). Solution for this is to use GraphQL Dataloader but this increase complexity

RPC

Client calls the function instead of REST API, for example

// Instead of GET /events/123
getEvent(eventId: "123")

// Instead of POST /events/123/bookings
createBooking(eventId: "123", userId: "456", tickets: [...])

// Instead of GET /events/123/tickets
getAvailableTickets(eventId: "123", section: "VIP")

It's very fast since it's using HTTP/2 for transport

Buffers and type safety

You write .proto file that describes your service methods

service TicketService {
  rpc GetEvent(GetEventRequest) returns (Event);
  rpc CreateBooking(CreateBookingRequest) returns (Booking);
  rpc GetAvailableTickets(GetTicketsRequest) returns (TicketList);
}

message GetEventRequest {
  string event_id = 1;
}

message Event {
  string id = 1;
  string name = 2;
  int64 date = 3;
  Venue venue = 4;
}

This is typesafe and your Go backend service can talk to your Java payment service. Compile time will check for type mismatch before deployment.

When to use gRPC

  1. Performance is critical
  2. Type safety matters
  3. Service-to-service communication
  4. Internal-service streaming: gRPC supports bidirectional stream for real-time features

[!note]
Unless explicitly asked, don't outline internal API at most we just say they communicate via gRPC

Common API Patterns

Pagination

Offset pagination

  • /events?offset=20&limit=10
  • /events?page=20&pageSize=10

These are the same thing, and get the record from 21-30

However if someone adds new event while you're paginating through this will create a problem

Cursor-based pagination

First request /events?limit=10

Response

{
  "events": [...],
  "next_cursor": "cmd9atj3p000007ky19w1dpy2"
}

Next request /events?cursor=cmd9atj3p000007ky19w1dpy2 it will return the { events, next_cursor } of the next response. in here cmd9atj3p000007ky19w1dpy2 is the last event

[!note]
Use cursor base for real-time data or high-volume scenario

Filtering and sorting

Use /search?keyword=something&condition=something

Idempotency keys

Say if we send POST to create new resources, if the connection lost, application retry and create a new POST. How do we make sure that this time we should return the result of the previous request?

We need to use Idempotency-Key in Request Header

POST /events/123/bookings
Idempotency-Key: 8e03978e-740c-4c2e-abd1-52f427d5c177
{
  "tickets": [{"section": "VIP", "quantity": 2}]
}

If the backend see the same Idempotency-Key, it should return the stored response instead of processing the request again. To do this we can either store the key in the database or redis for short term idempotency

Consistent error response

{
  "error": {
    "code": "SEAT_UNAVAILABLE",
    "message": "Section VIP has only 1 seat remaining"
  }
}

We need to reply something like this, the code is for the client to read and the message is to display. We keep this format across our apis

Versioning strategy

Normally client needs to match the code with the server api. Adding versioning allow us to modify API without having to worry if it breaks the client or not.

URL versioning

  • We can do something like /v1/events or /v2/events and if the client update, they can still call our /v1/events
  • This allow us to know which versioning the application using simply by taking a look at the URL

Header versioning

  • We can also put the versioning in the header Accept-Version: v2 or API-Version: 2. However it's less obvious and harder to test in browser

[!note]
In interview, use URL versioning for safe

Security considerations

Authentication vs Authorization

Authentication: verifies identity — proof that it's the correct user
Authorisation: verfies the permissions

API keys

Good for server-to-server communication of authentication. DO NOT USE FOR CLIENT FACING APP

  1. Your server generate an API key for the client, store this in the database with rate limits, permissions etc
  2. Client include their api keys in the HTTP header as of the Authorization
  3. Server go and check for that
GET /events
Authorization: Bearer sk_live_abc123...

[!note]
Bearer here is the standard to indicate that it's an access token, the others are Basic for username and password encoding or Digest — for a challenge response flow

JWT (Json Web Token)

See: JWT Access Token Refresh token

Encode the user info directly to the token with permissions, userId, expirationTime and then sign with a secret key. Any service with secret key can validate this tokens independently

Good for public/client access

RBAC (Role-based access control)

This handle the authorisation. We can have a few roles i.e

Roles:
- customer: can book tickets, view own bookings
- venue_manager: can create events, view sales for their venues
- admin: can access everything

User: [email protected] → Role: customer
User: [email protected] → Role: venue_manager
GET /bookings/{id}
1. Is the user authenticated? (valid JWT token)
2. Is the user authorized? (owns this booking OR is admin)

Rate limiting and throttling

Common strategies

  1. Per-user limit
  2. Per IP limit
  3. Endpoint specific limits