Resource-Based API Structure

Designing RESTful APIs Around Resources

Introduction to Resource-Based Design

At the heart of REST architecture is the concept of resources. A resource is any meaningful entity in your application that can be identified, named, and manipulated through representations. When designing RESTful APIs, organizing your endpoints around resources rather than actions creates a more intuitive, consistent, and maintainable API.

Think of resources as nouns in your application domain. For example, in a blog application, resources might include users, posts, comments, and categories. Each resource can be created, retrieved, updated, and deleted using standard HTTP methods - these are the verbs that act on your resource nouns.

This approach is analogous to how we interact with physical objects in the real world. Just as you can pick up (GET), create (POST), modify (PUT), or throw away (DELETE) a book, you can perform similar operations on digital resources in your API.

graph LR A[API] --> B[Resources] B --> C[Users] B --> D[Posts] B --> E[Comments] B --> F[Categories] C --> C1[GET /users] C --> C2[POST /users] C --> C3[GET /users/:id] C --> C4[PUT /users/:id] C --> C5[DELETE /users/:id] style B fill:#f9f,stroke:#333,stroke-width:2px

Identifying Resources

The first step in designing a resource-based API is identifying the key resources in your application. Resources typically correspond to the main entities or data models in your domain.

Guidelines for Identifying Resources

Types of Resources

Resource Type Description Example
Collection Resources Groups of similar items /users, /posts, /comments
Instance Resources Individual items within a collection /users/123, /posts/456, /comments/789
Nested Resources Resources that belong to other resources /users/123/posts, /posts/456/comments
Singleton Resources Resources that exist as a single instance /profile, /dashboard, /settings
Controller Resources Special resources that handle complex operations /search, /calculate-tax, /generate-report

Example: E-commerce Application Resources

Designing Resource URIs

Once you've identified your resources, you need to design URIs (Uniform Resource Identifiers) that will be used to access them. Well-designed URIs make your API intuitive and easy to use.

URI Design Principles

URI Patterns for Common Resources

Resource Type URI Pattern Examples
Collection /resources /users, /products, /orders
Instance /resources/:id /users/123, /products/xyz, /orders/789
Nested Collection /resources/:id/sub-resources /users/123/orders, /products/xyz/reviews
Nested Instance /resources/:id/sub-resources/:sub-id /users/123/orders/456, /products/xyz/reviews/789
Singleton /resource /profile, /cart, /settings
Controller /controller-name /search, /calculate, /validate

Query Parameters vs Path Parameters

When designing URIs, you need to decide when to use path parameters (e.g., /users/:id) and when to use query parameters (e.g., /users?role=admin).

As a general rule, if a parameter is required to identify the resource, it should be a path parameter. If it's optional or used for filtering, sorting, or other operations, it should be a query parameter.

HTTP Methods and CRUD Operations

In a RESTful API, HTTP methods (verbs) are used to perform operations on resources. The most common methods correspond to CRUD operations (Create, Read, Update, Delete).

Main HTTP Methods

HTTP Method CRUD Operation Description Example
GET Read Retrieve resource(s) GET /users, GET /users/123
POST Create Create a new resource POST /users
PUT Update Replace a resource completely PUT /users/123
PATCH Update (partial) Update parts of a resource PATCH /users/123
DELETE Delete Remove a resource DELETE /users/123

Less Common HTTP Methods

HTTP Method Description Example Use
HEAD Like GET but returns only headers, no body Check if a resource exists or has been modified
OPTIONS Get information about available methods CORS preflight requests, API discovery

HTTP Method Properties

Property Description Methods
Safe Does not modify resources GET, HEAD, OPTIONS
Idempotent Multiple identical requests have same effect as a single request GET, HEAD, PUT, DELETE, OPTIONS
Cacheable Responses can be cached GET, HEAD (POST can be in some cases)

Common Resource Operations

graph TD A[Collection Resource
/resources] --> B[GET
List all resources] A --> C[POST
Create a new resource] D[Instance Resource
/resources/:id] --> E[GET
Retrieve a specific resource] D --> F[PUT/PATCH
Update a resource] D --> G[DELETE
Remove a resource]

Understanding these properties is important for designing predictable and reliable APIs. For example, if a method is idempotent, clients can safely retry requests without worrying about unintended side effects.

Resource Relations and Nested Resources

Resources in an API often have relationships with other resources. For example, a user can have multiple orders, or a product can have multiple reviews. RESTful APIs typically represent these relationships through nested resources.

Types of Resource Relationships

Representing Relationships in URIs

  1. Nested Resources: Use hierarchical URIs to represent parent-child relationships
    Examples: /users/123/orders, /orders/456/items
  2. Reference Fields: Include reference IDs in resource representations
    Example: Order resource includes user_id to reference the owner
  3. Link Relations: Include links to related resources in representations
    Example: User resource includes a link to its orders collection

Examples of Nested Resources

# Get all orders for a specific user
GET /users/123/orders

# Get a specific order for a user
GET /users/123/orders/456

# Create a new order for a user
POST /users/123/orders

# Get all reviews for a product
GET /products/xyz/reviews

# Add a review to a product
POST /products/xyz/reviews

Avoiding Deep Nesting

While nested resources are useful for representing relationships, deep nesting can lead to overly complex URIs. As a general rule, try to limit nesting to one or two levels.

# Too deep - avoid this
GET /users/123/orders/456/items/789/attributes

# Better alternatives
GET /order-items/789
GET /order-items?order_id=456
GET /item-attributes?item_id=789

Resource Linking with HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) is a constraint of REST architecture where clients interact with the application entirely through hypermedia provided dynamically by the server. In practice, this means including links to related resources in API responses.

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": { "href": "/users/123" },
    "orders": { "href": "/users/123/orders" },
    "profile": { "href": "/users/123/profile" }
  }
}

This approach allows clients to navigate the API without having to hardcode URIs, making the API more resilient to changes.

Resource Representation

A resource representation is the format in which a resource is presented to clients. In RESTful APIs, representations are typically in JSON or XML, with JSON being the most common.

Elements of a Good Resource Representation

JSON Representation Example

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "role": "customer",
  "created_at": "2023-01-15T10:30:00Z",
  "updated_at": "2023-05-20T14:45:00Z",
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "state": "CA",
    "zip": "12345"
  },
  "orders_count": 5,
  "_links": {
    "self": { "href": "/users/123" },
    "orders": { "href": "/users/123/orders" }
  }
}

Collection Representation

Collections should be represented as arrays of objects, often with metadata about the collection:

{
  "total": 100,
  "count": 10,
  "page": 1,
  "per_page": 10,
  "items": [
    {
      "id": 123,
      "name": "John Doe",
      "email": "john@example.com",
      "_links": { "self": { "href": "/users/123" } }
    },
    {
      "id": 124,
      "name": "Jane Smith",
      "email": "jane@example.com",
      "_links": { "self": { "href": "/users/124" } }
    }
    // ... more items ...
  ],
  "_links": {
    "self": { "href": "/users?page=1&per_page=10" },
    "next": { "href": "/users?page=2&per_page=10" },
    "last": { "href": "/users?page=10&per_page=10" }
  }
}

Field Filtering and Projection

APIs should allow clients to request only the fields they need, reducing bandwidth and improving performance:

# Request only specific fields
GET /users/123?fields=id,name,email

# Response
{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com"
}

Content Negotiation

RESTful APIs should support content negotiation, allowing clients to request different representations of the same resource:

# Request JSON format
GET /users/123
Accept: application/json

# Request XML format
GET /users/123
Accept: application/xml

The server should respond with the requested format or return a 406 Not Acceptable status if it can't provide the requested format.

Implementing Resources in Flask-RESTful

Now let's see how to implement resource-based design in Flask-RESTful. We'll create a complete API for a task management application with tasks, categories, and users.

Domain Model

First, let's define our domain model:

Resource URIs

Now, let's define the URIs for our resources:

Resource URI Methods Description
Users Collection /users GET, POST List all users, Create a new user
User Instance /users/:id GET, PUT, DELETE Get, Update, or Delete a user
Categories Collection /categories GET, POST List all categories, Create a new category
Category Instance /categories/:id GET, PUT, DELETE Get, Update, or Delete a category
Tasks Collection /tasks GET, POST List all tasks, Create a new task
Task Instance /tasks/:id GET, PUT, DELETE Get, Update, or Delete a task
User Tasks /users/:id/tasks GET List all tasks for a user
Category Tasks /categories/:id/tasks GET List all tasks in a category
Task Completion /tasks/:id/complete PUT Mark a task as complete

Database Models

Let's implement the SQLAlchemy models for our resources:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Relationships
    tasks = db.relationship('Task', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
    categories = db.relationship('Category', backref='owner', lazy='dynamic', cascade='all, delete-orphan')
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
        
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def __repr__(self):
        return f''

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    description = db.Column(db.Text)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    
    # Relationships
    tasks = db.relationship('Task', backref='category', lazy='dynamic')
    
    def __repr__(self):
        return f''

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    due_date = db.Column(db.DateTime)
    completed = db.Column(db.Boolean, default=False)
    priority = db.Column(db.Integer, default=1)  # 1=Low, 2=Medium, 3=High
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Foreign keys
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
    
    def __repr__(self):
        return f''

Resource Fields for Marshaling

Now, let's define the field definitions for marshaling our resources:

from flask_restful import fields

# User fields
user_fields = {
    'id': fields.Integer,
    'username': fields.String,
    'email': fields.String,
    'created_at': fields.DateTime(dt_format='iso8601'),
    'tasks_count': fields.Integer(attribute=lambda x: x.tasks.count()),
    'categories_count': fields.Integer(attribute=lambda x: x.categories.count())
}

# Category fields
category_fields = {
    'id': fields.Integer,
    'name': fields.String,
    'description': fields.String,
    'user_id': fields.Integer,
    'tasks_count': fields.Integer(attribute=lambda x: x.tasks.count())
}

# Task fields
task_fields = {
    'id': fields.Integer,
    'title': fields.String,
    'description': fields.String,
    'due_date': fields.DateTime(dt_format='iso8601'),
    'completed': fields.Boolean,
    'priority': fields.Integer,
    'created_at': fields.DateTime(dt_format='iso8601'),
    'updated_at': fields.DateTime(dt_format='iso8601'),
    'user_id': fields.Integer,
    'category_id': fields.Integer,
    'category_name': fields.String(attribute=lambda x: x.category.name if x.category else None)
}

Resources Implementation

Now let's implement our resources in Flask-RESTful:

from flask import request, g
from flask_restful import Resource, marshal_with, reqparse, abort
from sqlalchemy import desc

# User resources
class UserResource(Resource):
    @marshal_with(user_fields)
    def get(self, user_id):
        user = User.query.get_or_404(user_id)
        return user
    
    @marshal_with(user_fields)
    def put(self, user_id):
        user = User.query.get_or_404(user_id)
        
        # In a real app, check permissions here
        
        parser = reqparse.RequestParser()
        parser.add_argument('username', type=str)
        parser.add_argument('email', type=str)
        args = parser.parse_args()
        
        if args['username']:
            user.username = args['username']
        if args['email']:
            user.email = args['email']
            
        db.session.commit()
        return user
    
    def delete(self, user_id):
        user = User.query.get_or_404(user_id)
        
        # In a real app, check permissions here
        
        db.session.delete(user)
        db.session.commit()
        return {'message': 'User deleted successfully'}, 200

class UserListResource(Resource):
    @marshal_with(user_fields)
    def get(self):
        users = User.query.all()
        return users
    
    @marshal_with(user_fields)
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('username', type=str, required=True, help='Username is required')
        parser.add_argument('email', type=str, required=True, help='Email is required')
        parser.add_argument('password', type=str, required=True, help='Password is required')
        args = parser.parse_args()
        
        # Check if username or email already exists
        if User.query.filter_by(username=args['username']).first():
            abort(409, message=f"Username {args['username']} already exists")
            
        if User.query.filter_by(email=args['email']).first():
            abort(409, message=f"Email {args['email']} already exists")
        
        user = User(username=args['username'], email=args['email'])
        user.set_password(args['password'])
        
        db.session.add(user)
        db.session.commit()
        
        return user, 201

class UserTasksResource(Resource):
    @marshal_with(task_fields)
    def get(self, user_id):
        user = User.query.get_or_404(user_id)
        
        # Parse query parameters for filtering
        parser = reqparse.RequestParser()
        parser.add_argument('completed', type=str, location='args')
        parser.add_argument('sort', type=str, location='args')
        args = parser.parse_args()
        
        # Start with all tasks for this user
        query = user.tasks
        
        # Filter by completion status if specified
        if args['completed'] is not None:
            completed = args['completed'].lower() == 'true'
            query = query.filter_by(completed=completed)
        
        # Sort tasks
        if args['sort'] == 'priority':
            query = query.order_by(desc(Task.priority))
        elif args['sort'] == 'due_date':
            query = query.order_by(Task.due_date)
        else:
            query = query.order_by(desc(Task.created_at))
        
        tasks = query.all()
        return tasks

# Category resources
class CategoryResource(Resource):
    @marshal_with(category_fields)
    def get(self, category_id):
        category = Category.query.get_or_404(category_id)
        return category
    
    @marshal_with(category_fields)
    def put(self, category_id):
        category = Category.query.get_or_404(category_id)
        
        # In a real app, check permissions here
        
        parser = reqparse.RequestParser()
        parser.add_argument('name', type=str)
        parser.add_argument('description', type=str)
        args = parser.parse_args()
        
        if args['name']:
            category.name = args['name']
        if args['description']:
            category.description = args['description']
            
        db.session.commit()
        return category
    
    def delete(self, category_id):
        category = Category.query.get_or_404(category_id)
        
        # In a real app, check permissions here
        
        db.session.delete(category)
        db.session.commit()
        return {'message': 'Category deleted successfully'}, 200

class CategoryListResource(Resource):
    @marshal_with(category_fields)
    def get(self):
        # Optionally filter by user_id
        user_id = request.args.get('user_id', type=int)
        
        if user_id:
            categories = Category.query.filter_by(user_id=user_id).all()
        else:
            categories = Category.query.all()
            
        return categories
    
    @marshal_with(category_fields)
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('name', type=str, required=True, help='Name is required')
        parser.add_argument('description', type=str)
        parser.add_argument('user_id', type=int, required=True, help='User ID is required')
        args = parser.parse_args()
        
        # Check if user exists
        user = User.query.get(args['user_id'])
        if not user:
            abort(404, message=f"User with id {args['user_id']} not found")
        
        category = Category(
            name=args['name'],
            description=args['description'],
            user_id=args['user_id']
        )
        
        db.session.add(category)
        db.session.commit()
        
        return category, 201

class CategoryTasksResource(Resource):
    @marshal_with(task_fields)
    def get(self, category_id):
        category = Category.query.get_or_404(category_id)
        tasks = category.tasks.all()
        return tasks

# Task resources
class TaskResource(Resource):
    @marshal_with(task_fields)
    def get(self, task_id):
        task = Task.query.get_or_404(task_id)
        return task
    
    @marshal_with(task_fields)
    def put(self, task_id):
        task = Task.query.get_or_404(task_id)
        
        # In a real app, check permissions here
        
        parser = reqparse.RequestParser()
        parser.add_argument('title', type=str)
        parser.add_argument('description', type=str)
        parser.add_argument('due_date', type=str)  # ISO format date string
        parser.add_argument('completed', type=bool)
        parser.add_argument('priority', type=int)
        parser.add_argument('category_id', type=int)
        args = parser.parse_args()
        
        # Update fields if provided
        if args['title']:
            task.title = args['title']
        if args['description'] is not None:  # Allow empty description
            task.description = args['description']
        if args['due_date']:
            try:
                task.due_date = datetime.fromisoformat(args['due_date'])
            except ValueError:
                abort(400, message="Invalid due_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)")
        if args['completed'] is not None:
            task.completed = args['completed']
        if args['priority']:
            task.priority = args['priority']
        if args['category_id'] is not None:
            # Check if category exists and belongs to the same user
            if args['category_id'] == 0:  # Special case: remove category
                task.category_id = None
            else:
                category = Category.query.get(args['category_id'])
                if not category:
                    abort(404, message=f"Category with id {args['category_id']} not found")
                if category.user_id != task.user_id:
                    abort(403, message="Category belongs to a different user")
                task.category_id = args['category_id']
            
        db.session.commit()
        return task
    
    def delete(self, task_id):
        task = Task.query.get_or_404(task_id)
        
        # In a real app, check permissions here
        
        db.session.delete(task)
        db.session.commit()
        return {'message': 'Task deleted successfully'}, 200

class TaskListResource(Resource):
    @marshal_with(task_fields)
    def get(self):
        # Parse query parameters for filtering
        parser = reqparse.RequestParser()
        parser.add_argument('user_id', type=int, location='args')
        parser.add_argument('category_id', type=int, location='args')
        parser.add_argument('completed', type=str, location='args')
        parser.add_argument('sort', type=str, location='args')
        args = parser.parse_args()
        
        # Start with base query
        query = Task.query
        
        # Apply filters if provided
        if args['user_id']:
            query = query.filter_by(user_id=args['user_id'])
        if args['category_id']:
            query = query.filter_by(category_id=args['category_id'])
        if args['completed'] is not None:
            completed = args['completed'].lower() == 'true'
            query = query.filter_by(completed=completed)
        
        # Apply sorting
        if args['sort'] == 'priority':
            query = query.order_by(desc(Task.priority))
        elif args['sort'] == 'due_date':
            query = query.order_by(Task.due_date)
        else:
            query = query.order_by(desc(Task.created_at))
        
        tasks = query.all()
        return tasks
    
    @marshal_with(task_fields)
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument('title', type=str, required=True, help='Title is required')
        parser.add_argument('description', type=str)
        parser.add_argument('due_date', type=str)  # ISO format date string
        parser.add_argument('priority', type=int, default=1)
        parser.add_argument('user_id', type=int, required=True, help='User ID is required')
        parser.add_argument('category_id', type=int)
        args = parser.parse_args()
        
        # Check if user exists
        user = User.query.get(args['user_id'])
        if not user:
            abort(404, message=f"User with id {args['user_id']} not found")
        
        # Check if category exists and belongs to the user
        if args['category_id']:
            category = Category.query.get(args['category_id'])
            if not category:
                abort(404, message=f"Category with id {args['category_id']} not found")
            if category.user_id != args['user_id']:
                abort(403, message="Category belongs to a different user")
        
        # Parse due_date if provided
        due_date = None
        if args['due_date']:
            try:
                due_date = datetime.fromisoformat(args['due_date'])
            except ValueError:
                abort(400, message="Invalid due_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)")
        
        task = Task(
            title=args['title'],
            description=args['description'],
            due_date=due_date,
            priority=args['priority'],
            user_id=args['user_id'],
            category_id=args['category_id']
        )
        
        db.session.add(task)
        db.session.commit()
        
        return task, 201

class TaskCompleteResource(Resource):
    @marshal_with(task_fields)
    def put(self, task_id):
        task = Task.query.get_or_404(task_id)
        
        # In a real app, check permissions here
        
        task.completed = True
        db.session.commit()
        
        return task

Registering the Resources

Finally, let's register our resources with the Flask-RESTful API:

from flask import Flask
from flask_restful import Api
from models import db
from resources import (
    UserResource, UserListResource, UserTasksResource,
    CategoryResource, CategoryListResource, CategoryTasksResource,
    TaskResource, TaskListResource, TaskCompleteResource
)

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    
    # Initialize extensions
    db.init_app(app)
    api = Api(app)
    
    # Register resources
    api.add_resource(UserListResource, '/users')
    api.add_resource(UserResource, '/users/')
    api.add_resource(UserTasksResource, '/users//tasks')
    
    api.add_resource(CategoryListResource, '/categories')
    api.add_resource(CategoryResource, '/categories/')
    api.add_resource(CategoryTasksResource, '/categories//tasks')
    
    api.add_resource(TaskListResource, '/tasks')
    api.add_resource(TaskResource, '/tasks/')
    api.add_resource(TaskCompleteResource, '/tasks//complete')
    
    # Create database tables
    with app.app_context():
        db.create_all()
    
    return app

if __name__ == '__main__':
    app = create_app()
    app.run(debug=True)

This implementation demonstrates a complete resource-based API structure for a task management application, following RESTful principles.

Best Practices for Resource Design

Consistency

Granularity

Versioning

Pagination

Filtering, Sorting, and Searching

Error Handling

Authentication and Authorization

Common API Design Patterns

CRUD Operations

The basic pattern for CRUD operations on a resource:

Operation HTTP Method URI Description
Create POST /resources Create a new resource
Read (List) GET /resources Get all resources
Read (Single) GET /resources/:id Get a specific resource
Update PUT/PATCH /resources/:id Update a resource
Delete DELETE /resources/:id Delete a resource

Controller Resources

Sometimes operations don't fit neatly into CRUD. Controller resources can be used for actions:

# Convert a document to PDF
POST /documents/123/convert?format=pdf

# Send a message
POST /messages/send

# Calculate tax
POST /calculator/tax

Keep controller resources focused on specific operations that don't fit the standard CRUD model.

Bulk Operations

For operating on multiple resources at once:

# Bulk create
POST /users/bulk
[
  {"username": "user1", "email": "user1@example.com"},
  {"username": "user2", "email": "user2@example.com"}
]

# Bulk update
PUT /users/bulk
[
  {"id": 1, "username": "new_user1"},
  {"id": 2, "username": "new_user2"}
]

# Bulk delete
DELETE /users?ids=1,2,3

Filtering and Searching

Approaches for filtering and searching resources:

# Basic filtering
GET /products?category=electronics&min_price=100&max_price=500

# Full-text search
GET /products/search?q=wireless+headphones

# Advanced filtering (complex criteria)
POST /products/filter
{
  "category": "electronics",
  "price": {"min": 100, "max": 500},
  "attributes": {"color": "black", "wireless": true}
}

Nested Resources vs. Query Parameters

Two approaches to handling related resources:

# Using nested resources
GET /users/123/orders          # Get all orders for user 123
GET /users/123/orders/456      # Get order 456 for user 123

# Using query parameters
GET /orders?user_id=123        # Get all orders for user 123
GET /orders/456?user_id=123    # Get order 456 with a user check

The nested approach is more RESTful and reflects resource relationships more clearly, while the query parameter approach is simpler and can avoid deep nesting.

Practical Activity: Designing a RESTful API

Let's put our knowledge into practice by designing a RESTful API for a simple e-commerce system.

Domain Model

Task

Design a resource-based API for this e-commerce system:

  1. Identify all resources (collections and instances)
  2. Design URI patterns for each resource
  3. Specify HTTP methods for each URI
  4. Define the representations (JSON structure) for each resource
  5. Consider relationships between resources
  6. Plan for filtering, sorting, and pagination
  7. Design any special endpoints or controller resources

This activity will help you apply the principles of resource-based API design to a realistic scenario.

Key Takeaways

Further Learning Resources