WEB API's Lesson 12 – Resource Modeling | Dataplexa
Web APIs · Lesson 12

Resource Modeling

Design clean resource structures that map perfectly to your domain model and scale with your application.
Twitter's API endpoints tell a story. GET /tweets/{id} fetches a tweet. GET /users/{id}/followers gets someone's followers. DELETE /tweets/{id} removes a tweet. Each URL maps perfectly to something real in their system. That clean mapping between URLs and actual things is resource modeling.

Most API design disasters happen right here. Developers jump straight into building endpoints without thinking through what their resources actually are. You end up with URLs like POST /getUserDataWithFollowersAndTweets or GET /api/data/process/user/123/info. These URLs scream "I didn't plan this."

Resource modeling forces you to think like your users. What concepts matter in your domain? How do they relate to each other? How would someone naturally expect to access them through a URL?

What Makes a Good Resource

Stripe charges your credit card through a handful of resources: customers, payment methods, charges, subscriptions. Each resource represents one clear concept from their business domain.

A resource is not a database table. A resource is not a function. A resource is a meaningful concept that clients want to interact with. Think nouns, not verbs. Think things, not actions.

Good resources share common traits. They represent something concrete that users understand. They have a clear lifecycle - created, read, updated, deleted. They can exist independently or relate to other resources in predictable ways.

Concept
Domain Entity
Used for
URL Pattern
HTTP Methods
Resource Type What it represents APIForge example
Entity Resource Single identifiable thing with properties A project, team member, deployment
Collection Resource Group of similar entities that can be listed All projects for a team, active deployments
Sub-resource Entity that belongs to or relates to parent Project environments, team permissions
Action Resource Process or operation with side effects Deploy action, password reset
The APIForge Backend team builds a developer platform where teams manage projects, deployments, and infrastructure. Their resources emerged naturally from what their users actually do. Users create projects. Projects contain environments. Environments have deployments. Each concept became a resource.

Resource Hierarchies and Relationships

GitHub's API shows resource relationships through URL structure. GET /repos/{owner}/{repo}/issues gets issues for a specific repository. GET /repos/{owner}/{repo}/issues/{number}/comments gets comments on that specific issue.

The URL path tells you exactly how these resources connect. Repositories contain issues. Issues contain comments. The hierarchy flows naturally from left to right in the URL.

Not every relationship needs to show up in URLs though. Sometimes you model relationships through resource properties instead of nested paths. A user might belong to multiple teams, but GET /users/123 would include team IDs in the response rather than requiring GET /teams/{id}/users/{id}.

Resource Hierarchy Rules
Parent-child relationships work best as nested URLs when the child cannot exist without the parent. Peer relationships work better as resource properties with IDs that reference other resources.
The APIForge Backend team models their platform resources with clear hierarchies:
/teams/{team-id}                    # Team entity
/teams/{team-id}/projects           # Projects collection
/teams/{team-id}/projects/{id}      # Specific project
/projects/{id}/environments         # Project environments
/projects/{id}/environments/{env}/deployments  # Deployments in environment
/deployments/{deployment-id}        # Individual deployment
/deployments/{deployment-id}/logs   # Deployment logs
GET /teams/tg847/projects/web-frontend HTTP/1.1 Host: api.apiforge.dev Authorization: Bearer eyJ0eXAiOiJKV1... HTTP/1.1 200 OK Content-Type: application/json { "id": "web-frontend", "name": "Web Frontend", "team_id": "tg847", "created_at": "2024-01-15T10:30:00Z", "environments": ["dev", "staging", "prod"], "repository": "github.com/apiforge/web-frontend", "_links": { "team": "/teams/tg847", "environments": "/projects/web-frontend/environments" } }
What just happened?
The project resource lives under its team but includes links to related resources. The URL hierarchy shows ownership while the response provides navigation paths. This lets clients discover related resources without guessing URL patterns.

Resource Naming Conventions

Slack's API URLs read like natural language. GET /conversations.list gets conversation lists. POST /chat.postMessage posts a chat message. The naming follows patterns that developers can predict.

Good resource names use plural nouns for collections and specific identifiers for individual items. You want URLs that make sense when read aloud. "Get all users" maps to GET /users. "Get user 123" maps to GET /users/123.

Avoid implementation details in resource names. Don't call it /user-table or /customer-records. Don't use verbs like /getUsers or /updateCustomer. The HTTP method already tells you the action.

Good Resource Names
/users
/users/123
/projects
/projects/web-app/environments
/orders/456/line-items
Naming Rules
Use plural nouns for collections
Use lowercase with hyphens
Avoid verbs in URLs
Keep paths short and logical
Be consistent across your API
The APIForge team follows consistent naming patterns across their entire platform. Teams contain projects. Projects contain environments. Environments contain deployments. Each level uses predictable plural nouns and clear hierarchies.

Resource State and Lifecycle

Twilio phone numbers have states: available, reserved, assigned, released. Each state determines what operations make sense. You can reserve an available number, assign a reserved number, or release an assigned number.

Resource state drives API behavior. A draft blog post accepts edits. A published post might reject certain changes. A deleted post returns 404 errors. Your resource model needs to account for these state transitions.

Some resources have simple lifecycles - created, updated, deleted. Others have complex workflows with multiple states and business rules about valid transitions. Document these states clearly because they affect how clients interact with your API.

The APIForge deployments resource demonstrates state-driven behavior:
POST /projects/web-frontend/environments/staging/deployments
Content-Type: application/json

{
  "version": "v2.1.4",
  "commit_sha": "a8f3d92",
  "auto_promote": false
}
HTTP/1.1 201 Created Content-Type: application/json Location: /deployments/dep_8x9y2k { "id": "dep_8x9y2k", "status": "pending", "version": "v2.1.4", "environment": "staging", "created_at": "2024-01-15T14:30:00Z", "steps": [ {"name": "build", "status": "queued"}, {"name": "test", "status": "queued"}, {"name": "deploy", "status": "queued"} ], "actions": { "cancel": "/deployments/dep_8x9y2k/cancel", "logs": "/deployments/dep_8x9y2k/logs" } }
What just happened?
The deployment resource starts in "pending" status with available actions based on its current state. As the deployment progresses, the status changes and different actions become available. This state-driven approach guides client behavior.
State Transition Patterns
Include current state in resource representations. Provide available actions for the current state. Use meaningful status codes when operations aren't allowed. Consider using state machines to model complex workflows.

Resource Composition and Embedding

Google Maps API lets you request different levels of detail for the same resource. GET /place/{id} returns basic information. GET /place/{id}?fields=name,geometry,photos returns specific fields. This flexible composition reduces API calls while keeping responses focused.

Resource composition decides what information gets included in API responses. Should GET /users/123 include the user's recent posts? Their team memberships? Just basic profile data?

The answer depends on client needs and performance constraints. Including too much creates bloated responses that waste bandwidth. Including too little forces clients to make multiple API calls to get complete information.

Many APIs solve this through field selection parameters or resource expansion options. Clients can request exactly the data they need without getting everything.
# Basic project info
GET /projects/web-frontend

# Project with environments expanded
GET /projects/web-frontend?expand=environments

# Project with specific fields only
GET /projects/web-frontend?fields=name,status,last_deployment
GET /projects/web-frontend?expand=environments HTTP/1.1 HTTP/1.1 200 OK Content-Type: application/json { "id": "web-frontend", "name": "Web Frontend", "status": "active", "environments": [ {"name": "dev", "url": "https://dev-frontend.apiforge.dev", "last_deployment": "2024-01-15T10:00:00Z"}, {"name": "staging", "url": "https://staging-frontend.apiforge.dev", "last_deployment": "2024-01-14T16:30:00Z"}, {"name": "prod", "url": "https://frontend.apiforge.dev", "last_deployment": "2024-01-12T14:15:00Z"} ] }
What just happened?
The expand parameter tells the API to include environment details directly in the project response instead of just providing links. This reduces round trips for clients that need this information immediately. Try this: Design your resources with sensible defaults but allow clients to request more or less detail as needed.

Action Resources and RPC-Style Operations

Not everything maps cleanly to CRUD operations on resources. Stripe handles this with action resources like POST /charges/{id}/capture or POST /customers/{id}/sources. These URLs represent operations rather than entities.

Password resets don't fit the standard resource model. Neither do email sends, report generations, or batch operations. These actions need their own resource representations even though they're really remote procedure calls.

Action resources work best when the operation has side effects or takes significant time. Quick computations can stay as query parameters, but operations that change system state or trigger workflows deserve their own endpoints.

The APIForge team handles deployment operations as action resources:
POST /deployments/dep_8x9y2k/rollback
Content-Type: application/json

{
  "reason": "Critical bug in payment processing",
  "target_version": "v2.1.3"
}
HTTP/1.1 202 Accepted Content-Type: application/json Location: /deployments/dep_nx4p7m { "rollback_deployment_id": "dep_nx4p7m", "status": "initiated", "reason": "Critical bug in payment processing", "target_version": "v2.1.3", "estimated_duration": "5 minutes", "progress_url": "/deployments/dep_nx4p7m/status" }
What just happened?
The rollback action creates a new deployment resource that represents the rollback operation. The 202 status code indicates the operation was accepted but hasn't completed yet. Clients can track progress through the provided URL. Action resources let you model complex operations while maintaining RESTful principles.

Resource Versioning and Evolution

Twitter's API evolved from simple tweet resources to rich media objects with threads, spaces, and communities. The resource model grew without breaking existing clients through careful versioning and backward compatibility.

Resources change over time. New properties get added. Old properties get deprecated. Entire resource types might get replaced with better models. Your initial resource design won't be your final design.

Plan for evolution from the start. Use resource versioning strategies that let you introduce changes gradually. Consider how clients will migrate from old resource formats to new ones.

Evolution Strategies
Add new optional properties without versioning. Use separate versioned endpoints for breaking changes. Provide migration guides when deprecating old resource formats. Consider resource aliases that map old URLs to new resource structures.

Common Resource Modeling Mistakes

The biggest resource modeling mistake is thinking in terms of your database schema instead of your domain concepts. Just because you have a users table doesn't mean you need a users resource that exposes every database column.

Resources represent concepts that matter to clients, not database implementation details. A single resource might combine data from multiple tables. Multiple resources might expose different views of the same underlying data.

Another common mistake is creating resources that are too granular or too coarse. GET /users/123/profile/name/first is too granular. GET /dashboard-data-dump is too coarse. Find the right level of abstraction for your clients.

Resource Anti-Patterns
URLs that mirror database tables
Resources with single-use endpoints
Exposing internal implementation details
Inconsistent naming across resources
Resources that require specific call sequences
Good Resource Design
Models domain concepts clearly
Uses consistent naming patterns
Provides appropriate levels of detail
Supports common client workflows
Evolves without breaking changes
Resource modeling shapes how developers think about your API. Get it right and your API feels intuitive. Get it wrong and every integration becomes a struggle. The investment in thoughtful resource design pays dividends in developer experience and API adoption.

Quiz

1. The APIForge team is designing URLs for their project management API. Which approach follows REST resource naming conventions?

2. APIForge deployments belong to specific environments and cannot exist without them. How should this relationship be modeled in the resource hierarchy?

3. APIForge needs to let users rollback deployments, which involves complex workflow logic that doesn't fit standard CRUD operations. What's the best way to model this?

Up Next
CRUD Operations
APIForge implements the four essential operations that bring their resource model to life.