Skip to content

Resource Relationships in REST — Sub-Resources, Links, and Nested Routes

DodaTech Updated 2026-06-28 4 min read

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.

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.

If /users/{id}/orders exists, each order response should link back to its user. Bidirectional navigation makes APIs discoverable.

Practice Questions

  1. When should you use sub-resources vs query parameters?
  2. What is the N+1 query problem in resource embedding?
  3. How do you model a many-to-many relationship?
  4. What is the recommended maximum nesting depth?
  5. When should you embed vs link related resources?

Answers

  1. 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

What is a sub-resource in REST?

A resource nested under another resource indicating a parent-child relationship, like /users/123/orders.

When should I embed related resources?

When the related data is small and almost always needed alongside the parent resource.

How deep should I nest resources?

No more than 3 levels. Deeper relationships should use query parameters or links.

What is the N+1 problem?

When fetching N items causes N+1 database queries because related data is loaded per item.

How do you model many-to-many in REST?

Use intermediate endpoints like /users/123/roles or explicit association resources.

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