Resource Relationships in REST — Sub-Resources, Links, and Nested Routes
In this tutorial, you will learn about Resource Relationships in REST. We cover key concepts, practical examples, and best practices to help you master this topic.
Resource relationships in REST APIs are modeled using nested sub-resources for one-to-many relationships, embedded representations for performance, and hypermedia links for loose coupling between related resources.
What You'll Learn
- How to model one-to-many and many-to-many relationships
- When to use sub-resources vs embedded resources vs links
- How to design nested routes for related data
Why It Matters
Poorly designed resource relationships lead to chatty APIs (too many round trips) or monolithic responses (too much data). Choosing the right relationship model balances completeness with performance, making your API both usable and efficient.
Real-World Use
DodaTech's API models user relationships: /users/{id}/orders lists orders (sub-resource), order responses embed order items for efficiency (embedded), and user responses link to the user's subscription (linked) because subscriptions are optional and rarely needed together.
flowchart LR
U["/users/123"] -- sub-resource --> O["/users/123/orders"]
U -- linked --> S["/subscriptions/456"]
O -- embedded --> I["Order Items\n(inline)"]
O -- linked --> P["/payments/789"]
style U fill:#dbeafe,stroke:#2563eb
style O fill:#fef3c7,stroke:#d97706
style S fill:#bbf7d0,stroke:#16a34a
Sub-Resources (Nested Routes)
from flask import Flask, jsonify
app = Flask(__name__)
# One-to-many: user has many orders
@app.route('/api/users/<int:user_id>/orders', methods=['GET'])
def get_user_orders(user_id):
orders = database.get_orders_by_user(user_id)
return jsonify({
"data": orders,
"_links": {
"user": {"href": f"/api/users/{user_id}"},
"self": {"href": f"/api/users/{user_id}/orders"}
}
})
# Two levels of nesting when needed
@app.route('/api/orders/<int:order_id>/items/<int:item_id>', methods=['GET'])
def get_order_item(order_id, item_id):
item = database.get_order_item(order_id, item_id)
if not item:
return jsonify({"error": "not_found"}), 404
return jsonify({
"data": item,
"_links": {
"order": {"href": f"/api/orders/{order_id}"},
"self": {"href": f"/api/orders/{order_id}/items/{item_id}"}
}
})
Embedded vs Linked Resources
@app.route('/api/orders/<int:order_id>', methods=['GET'])
def get_order(order_id):
order = database.get_order(order_id)
# Choose representation based on query parameter
embed = request.args.get('embed', '').split(',')
response = {
"id": order.id,
"status": order.status,
"total": order.total,
"created_at": order.created_at.isoformat(),
"_links": {
"self": {"href": f"/api/orders/{order.id}"},
"customer": {"href": f"/api/users/{order.user_id}"},
}
}
# Embedded items (always - they're small and almost always needed)
response["items"] = [
{
"id": item.id,
"product_id": item.product_id,
"quantity": item.quantity,
"price": item.price,
"_links": {
"self": {"href": f"/api/orders/{order.id}/items/{item.id}"},
"product": {"href": f"/api/products/{item.product_id}"}
}
}
for item in order.items
]
# Optionally embed payment data
if 'payment' in embed and order.payment_id:
payment = database.get_payment(order.payment_id)
response["payment"] = {
"id": payment.id,
"status": payment.status,
"amount": payment.amount,
"method": payment.method
}
else:
response["_links"]["payment"] = {
"href": f"/api/payments/{order.payment_id}"
}
return jsonify(response)
Many-to-Many Relationships
@app.route('/api/users/<int:user_id>/roles', methods=['GET'])
def get_user_roles(user_id):
"""Users can have many roles, roles can have many users"""
roles = database.get_roles_for_user(user_id)
return jsonify({
"data": [
{
"id": role.id,
"name": role.name,
"permissions": role.permissions,
"_links": {
"self": {"href": f"/api/roles/{role.id}"},
"users": {"href": f"/api/roles/{role.id}/users"}
}
}
for role in roles
],
"_links": {
"user": {"href": f"/api/users/{user_id}"},
"self": {"href": f"/api/users/{user_id}/roles"}
}
})
@app.route('/api/users/<int:user_id>/roles', methods=['POST'])
def assign_user_role(user_id):
role_id = request.json.get('role_id')
database.assign_role(user_id, role_id)
return jsonify({"status": "assigned"}), 201
Common Mistakes
1. Nesting Too Deep
More than 3 levels of nesting (/a/b/c/d) creates complex URIs and reduces cache effectiveness. Use query parameters or links for deeper relationships.
2. Always Embedding Related Resources
Embedding large related collections makes responses bloated. Use links for optional or large related data.
3. Ignoring the N+1 Query Problem
Embedding resources per item in a list triggers N+1 database queries. Batch load related data before Serialization.
4. Inconsistent Nesting Patterns
Mixing /user/123/orders and /orders?userId=123 confuses clients. Choose one pattern and use it consistently.
5. Not Providing Bidirectional Links
If /users/{id}/orders exists, each order response should link back to its user. Bidirectional navigation makes APIs discoverable.
Practice Questions
- When should you use sub-resources vs query parameters?
- What is the N+1 query problem in resource embedding?
- How do you model a many-to-many relationship?
- What is the recommended maximum nesting depth?
- When should you embed vs link related resources?
Answers
- Sub-resources for primary ownership (user owns orders); query parameters for filtering across resources. 2. When embedding resources per item in a list triggers separate database queries for each item. 3. Through nested endpoints under either resource or dedicated association resources. 4. 3 levels maximum. 5. Embed small, always-needed data; link large or optional related data.
Challenge
Build a REST API for a university system with: students, courses, enrollments (many-to-many), assignments (sub-resource of courses), and submissions (sub-resource of enrollments + assignments). Support ?embed=instructor,submissions query parameter for controlling embedding.
FAQ
Mini Project
Build a REST API for a project management system with: projects, tasks, users, comments, and tags. Model the relationships appropriately: sub-resources for owned data (project tasks), embedded for small data (task tags), linked for large data (task comments), and support embed query parameter for performance tuning.
What's Next
- Learn about HTTP methods reference for RESTful CRUD
- Explore GET guidelines for safe, idempotent requests
- Continue to POST vs PUT vs PATCH for resource mutation
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro