Skip to content

Resource Modeling

TL;DR

REST APIs are organized around resources (nouns like "events" and "bookings"), not actions (verbs like "createBooking"). Model your API as a collection of things you can interact with, use plural nouns for endpoints, and use nesting to express parent-child relationships.

The Mental Shift: From Actions to Filing Cabinets

Most developers start building APIs by thinking about what the system does. "The user creates a booking." "The admin cancels an order." "The system sends a notification."

That's natural — you're thinking about behavior. But REST asks you to think differently. Instead of designing endpoints around actions, you design them around things.

Imagine you're organizing an office with filing cabinets. You don't label your drawers "Create Invoice" or "Cancel Order." You label them Invoices, Orders, Customers. Then you interact with those drawers — you add an invoice, you pull out an order, you update a customer record.

REST works the same way. Your API endpoints are the labels on the filing cabinets. The HTTP methods (GET, POST, PUT, DELETE) are how you interact with what's inside.

This is the single most important mental shift in REST API design: your URLs describe things, not actions.

REST resource hierarchy

The Golden Rule: Plural Nouns, Never Verbs

Here's the rule that makes REST URLs immediately recognizable:

Bad (verb-based) Good (noun-based)
POST /createEvent POST /events
GET /getEventById?id=123 GET /events/123
POST /deleteUser DELETE /users/456
PUT /updateTicket PUT /tickets/789
GET /fetchAllBookings GET /bookings

Notice the pattern? Every good endpoint is a plural noun. The HTTP method tells you the action — you don't need to repeat it in the URL.

Why plural? Because the endpoint represents a collection. /events is the collection of all events. /events/123 is one specific event inside that collection. It reads naturally: "Give me all events" (GET /events) or "Give me event 123" (GET /events/123).

Worked Example: Modeling a Ticketmaster-Like API

Let's say you're designing APIs for a platform like Ticketmaster. What are the things in this domain?

Start by listing the nouns you'd find in the product requirements:

  • Events — concerts, sports games, theater shows
  • Venues — stadiums, arenas, theaters
  • Tickets — individual seats available for purchase
  • Bookings — a user's confirmed purchase of tickets
  • Users — the people using the platform

Each of these becomes a top-level resource:

GET    /events          # List all events
POST   /events          # Create a new event
GET    /events/123      # Get a specific event
PUT    /events/123      # Update an event
DELETE /events/123      # Delete an event

GET    /venues           # List all venues
GET    /venues/456       # Get a specific venue

GET    /users/789        # Get a specific user

That's the foundation. But now things get interesting — how do these resources relate to each other?

Nested Resources: When Relationships Are Required

A booking doesn't exist in a vacuum. Every booking is for a specific event. This is a required parent-child relationship — a booking without an event doesn't make sense.

For required relationships like this, you nest the child under the parent:

POST   /events/123/bookings       # Create a booking for event 123
GET    /events/123/bookings       # List all bookings for event 123
GET    /events/123/bookings/456   # Get a specific booking for event 123

Why is this better than a flat URL? Because the path itself tells you the relationship. When you read POST /events/123/bookings, you immediately know:

  1. We're creating a booking
  2. It's for event 123
  3. The event must exist for this to work

The path encodes structural information — the parent-child hierarchy is explicit in the URL.

The Nesting Debate: POST /bookings/:eventId vs POST /events/:eventId/bookings

This is a common discussion that comes up in both design reviews and interviews. Which is more RESTful?

# Option A: Flat with event ID as path param
POST /bookings/123

# Option B: Nested under the parent resource
POST /events/123/bookings

Option B is more RESTful. Here's why:

In Option A, /bookings/123 looks like you're accessing booking with ID 123 — that's the standard REST convention for "get a specific resource by its ID." It's ambiguous. Is 123 the booking ID or the event ID?

In Option B, the URL reads like a sentence: "For event 123, create a booking." The hierarchy is crystal clear. The path segment after a collection name always means "the resource with this ID in that collection."

This doesn't mean Option A is wrong — some APIs use it successfully. But in an interview or design review, Option B shows you understand RESTful resource hierarchy.

Flat Resources + Query Parameters: When Relationships Are Optional

Not every relationship needs nesting. Sometimes you want to filter or search across resources without requiring a parent.

Example: "Show me all tickets for event 123 in the VIP section."

GET /tickets?event_id=123&section=VIP

Here, event_id and section are filters, not structural identifiers. You could also ask for:

GET /tickets?section=VIP                  # All VIP tickets across all events
GET /tickets?price_max=100                # All tickets under $100
GET /tickets?event_id=123                 # All tickets for event 123

The query parameters are optional modifiers. The endpoint /tickets makes sense on its own — it's the collection of all tickets. The parameters just narrow down which ones you want.

The Decision Rule: Nest or Flatten?

Here's the simple rule:

Use nesting when... Use query params when...
The child can't exist without the parent The relationship is optional
The parent is required to identify the child You're filtering or searching
The hierarchy is always the same Users might filter by different criteria
There's only one level of nesting You'd need 3+ levels of nesting

Examples:

# Nesting — bookings REQUIRE an event
POST /events/123/bookings

# Query params — tickets can be searched many ways
GET /tickets?event_id=123&section=VIP&sort=price

# Nesting — venue schedule REQUIRES a venue
GET /venues/456/events

# Query params — global event search
GET /events?city=NYC&date=2025-03-15&genre=rock

One important caveat: don't go deeper than two levels of nesting. GET /events/123/bookings/456 is fine. GET /venues/1/events/123/bookings/456/tickets/789 is a nightmare. If you find yourself going three levels deep, flatten it:

# Too deeply nested
GET /venues/1/events/123/bookings/456

# Better — flatten and use the booking ID directly
GET /bookings/456

More Modeling Examples: Tricky Real-World Cases

Not every API concept maps cleanly to a "thing." Here are some cases that trip people up.

Actions That Don't Feel Like Resources

What about "liking" a post on Instagram, or "swiping right" on Tinder? These feel like actions, not things.

The trick is to turn the action into a resource:

Action Resource
"Like a post" POST /posts/123/likes (create a like resource)
"Unlike a post" DELETE /posts/123/likes/mine (delete the like resource)
"Follow a user" POST /users/456/followers (create a follower resource)
"Swipe right" POST /users/789/swipes with {"direction": "right"}
"Send a friend request" POST /users/456/friend-requests

See the pattern? A "like" is a thing. A "follow" is a thing. A "swipe" is a thing. You create them, you delete them, you can list them. Once you see them as nouns, the REST design falls into place.

Search as a Resource

Global search across multiple resource types doesn't fit neatly into a single collection. Many APIs handle this with a dedicated search endpoint:

GET /search?q=taylor+swift&type=events,venues

This is perfectly fine. Pragmatism beats purity. The important thing is that the results are well-structured JSON, and the endpoint follows the same conventions as the rest of your API.

Complete Ticketmaster API: Putting It All Together

Here's what a well-designed Ticketmaster-like API looks like:

# Events
GET    /events                          # List/search events
POST   /events                          # Create event (admin)
GET    /events/123                      # Get event details
PUT    /events/123                      # Update event (admin)
DELETE /events/123                      # Delete event (admin)

# Venues
GET    /venues                          # List venues
GET    /venues/456                      # Get venue details
GET    /venues/456/events               # Events at this venue

# Tickets
GET    /tickets?event_id=123            # Tickets for an event
GET    /tickets?section=VIP&price_max=200  # Search tickets
GET    /tickets/789                     # Get specific ticket

# Bookings (nested under events — required relationship)
POST   /events/123/bookings            # Create a booking
GET    /events/123/bookings            # List bookings for event
GET    /bookings/456                   # Get specific booking (flat for convenience)
DELETE /bookings/456                   # Cancel a booking

# Users
GET    /users/me                        # Current user profile
GET    /users/me/bookings               # Current user's bookings

Notice the mix of nested and flat endpoints. Bookings are nested under events for creation (because the event is required), but you can also access a booking directly by its ID for convenience. This is a common and practical pattern.

Path Parameters vs Query Parameters: The Final Rule

When should something go in the path versus the query string? Here's the definitive rule:

Path Parameters Query Parameters
Identify a specific resource Filter or modify the results
Required Optional
Structural Contextual
/events/123 — which event ?city=NYC — which events to include
/users/me/bookings — whose bookings ?status=confirmed — which bookings to show

If removing the parameter makes the URL meaningless, it belongs in the path. If removing it just gives you a broader result set, it belongs in the query string.

Interview Tip

In system design interviews, interviewers care far more about whether you identified the right resources than whether your URLs are pixel-perfect. Spending 30 seconds listing "our main resources are events, venues, tickets, and bookings" and then sketching out the endpoints shows maturity. Agonizing over /api/v1/events vs /api/v1/event for five minutes does not. Get the resources right, and the URLs practically write themselves.


When Nouns Don't Work: Google's Custom Method Pattern

REST says "think in nouns." But sometimes you genuinely need an action that doesn't map to CRUD. How do you "restart" a VM? "Cancel" an order that's already confirmed? "Star" a repository?

Forcing these into CRUD creates awkward designs:

# Awkward CRUD workarounds:
POST /vms/123/restarts          # Creating a "restart" resource? Weird.
PATCH /orders/456 { "status": "cancelled" }  # Works, but hides domain logic.
PUT /repos/789/stars/me         # Creating a "star" resource for the current user? Stretching it.

Google's API Design Guide solves this with custom methods — a colon separates the resource from the action:

POST /vms/123:restart
POST /orders/456:cancel
POST /repos/789:star
POST /documents/abc:translate
POST /files/report.pdf:watch

The pattern is POST /resource/{id}:action. The colon clearly signals "this is a custom action, not a sub-resource." The HTTP method is always POST (since the action has side effects).

When to use custom methods: - The operation doesn't fit cleanly into Create/Read/Update/Delete - Forcing it into PATCH would hide important domain logic - The action name is a well-known domain verb (restart, cancel, publish, archive)

When NOT to use custom methods: - The action can naturally be expressed as creating or updating a resource - You're just being lazy about resource modeling

Interview Tip

If an interviewer asks "how would you model restarting a VM?", saying POST /vms/123:restart shows you know Google's API conventions. It's cleaner than creating fake resources and demonstrates that you can break the "nouns-only" rule elegantly when the situation calls for it.


Resources and Data Modeling

API resources don't exist in a vacuum — they often map to (or intentionally diverge from) your database tables.

A common mistake is making your API a 1:1 mirror of your database schema. If you have tables users, user_addresses, and user_preferences, exposing three separate endpoints creates a chatty, fragile API that breaks when your schema changes.

Instead, think of your API resource as a business entity that may aggregate data from multiple tables:

# Bad: API mirrors database tables
GET /users/123
GET /user_addresses?user_id=123
GET /user_preferences?user_id=123

# Good: API models the business entity
GET /users/123
{
  "id": 123,
  "name": "Jane Doe",
  "address": { "city": "NYC", "zip": "10001" },
  "preferences": { "theme": "dark", "notifications": true }
}

The API is an abstraction layer. Under the hood, the server joins three tables to compose one response. If you later merge user_preferences into the users table, no client breaks.

This principle also applies in reverse: a single database table might be exposed as multiple API resources if the business domain requires it.

Interview Expectations: Junior vs. Senior

  • Junior/Mid-level: Might propose action-based URLs (POST /createBooking) or flat structures that don't capture relationships well (POST /bookings?eventId=123).
  • Senior/Staff: Implements strict noun-based resources. Automatically nests dependent resources to define clear hierarchies (POST /events/123/bookings). Easily identifies when CRUD verbs aren't enough and elegantly applies custom methods (like Google's POST /vms/123:restart) instead of forcing awkward resource models.