API Design
API Types

| API Type | Description | Use when |
|---|---|---|
| REST | Use standard HTTP method | Defautl go to, when need to expose to client |
| GraphQL | Single 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 |
| RPC | RPC 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
GET: retrieve only, no changePOST: create new resources, NOT safe and NOT Idempotent — Calling multiple time create multiple resourcesPUT: replace entire resource, howeverPUTIS IdempotentPATCH: Patch update part of the result.PATCHis not guarantee to be Idempotent.PATCHto update an email field is idempotentPATCHto append to a list is not idempotent
DELETE: remove the entire resource
Passing data to apis
- Path parameter:
/events/123— use when the value is required to identify the resource - Query parameter:
/eents?city=NYC&date=2024-01-01&...— use when search, filter, sort- These goes with
?and then follow by&…
- These goes with
- Request body: use to update/create a resource
GraphQL
Good:
- when different client may need different data than the api provided. Avoid "over-fetching" and "under-fetching".
- 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: - Complexity: need to implement schema validation, parsing
- Sophisticated caching strategies
- 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
- Query 100 events using 1 query getAll()
- 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
- Performance is critical
- Type safety matters
- Service-to-service communication
- 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/eventsor/v2/eventsand 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: v2orAPI-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
- Your server generate an API key for the client, store this in the database with rate limits, permissions etc
- Client include their api keys in the HTTP header as of the
Authorization - Server go and check for that
GET /events
Authorization: Bearer sk_live_abc123...
[!note]
Bearerhere is the standard to indicate that it's an access token, the others areBasicfor username and password encoding orDigest— 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
- Per-user limit
- Per IP limit
- Endpoint specific limits