Path Rewrites
When you rename an endpoint's URL path, you need to rewrite requests from old clients so they reach the new endpoint. Path rewrites handle this transparently.
When to Use Path Rewrites
- Renaming a resource:
/persons→/people - Restructuring URLs:
/users/{id}/posts→/posts?user_id={id} - Fixing inconsistent naming:
/getUser→/users/{id}
Basic Example
You're renaming /persons/{person_id} to /people/{person_id}:
# Old (v1)
@router.get("/persons/{person_id}", response=PersonOut)
def get_person(request, person_id: int):
return Person.objects.get(id=person_id)
# New (v2)
@router.get("/people/{person_id}", response=PersonOut)
def get_person(request, person_id: int):
return Person.objects.get(id=person_id)
Generate the Migration
The migration will detect that an operation was removed (/persons/{person_id}) and added (/people/{person_id}) with
the same operation_id. It auto-generates a PathRewrite:
data_migrations = DataMigrationSet(
path_rewrites=[
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
methods=["get"],
),
],
)
How It Works
When a v1 client requests:
The middleware:
- Detects v1 request
- Rewrites path to
/api/people/123 - Routes to the new endpoint
- Returns the response (applying any schema transformers)
The client never sees the path change.
Path Parameters
Path parameters use {name} syntax. Parameter values are preserved during rewriting:
Request /persons/42 → /people/42
Parameter Renaming
You can rename parameters by using different names:
The value is transferred to the new parameter name.
Method-Specific Rewrites
By default, a PathRewrite applies to all HTTP methods. Use methods to restrict:
# Only rewrite GET requests
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
methods=["get"],
)
# Rewrite GET, PUT, DELETE but not POST
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
methods=["get", "put", "delete"],
)
Multiple Rewrites
You can have multiple path rewrites in a single migration:
data_migrations = DataMigrationSet(
path_rewrites=[
PathRewrite(
old_path="/persons",
new_path="/people",
methods=["get", "post"],
),
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
methods=["get", "put", "delete"],
),
],
)
Combining with Schema Changes
Path rewrites often accompany schema changes. The same migration can have both:
data_migrations = DataMigrationSet(
path_rewrites=[
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
),
],
schema_downgrades=[
SchemaDowngrade("#/components/schemas/PersonOut", downgrade_person),
],
schema_upgrades=[
SchemaUpgrade("#/components/schemas/PersonIn", upgrade_person),
],
)
Order of operations:
- Path rewrite (request URL modified)
- Request upgrade (body transformed)
- Endpoint called
- Response downgrade (body transformed back)
Automatic Detection
The migration generator detects path renames automatically by matching:
- Same
operation_id(the function name by default) - Same HTTP method
- Different paths
If your rename isn't detected, ensure the endpoint function name stayed the same.
Manual Path Rewrites
For complex restructuring that isn't auto-detected, add rewrites manually:
# Complex restructure: nested resource to query param
# /users/{user_id}/posts → /posts?user_id={user_id}
data_migrations = DataMigrationSet(
path_rewrites=[
PathRewrite(
old_path="/users/{user_id}/posts",
new_path="/posts",
methods=["get"],
),
],
operation_upgrades=[
# Also need to transform the request to add query param
OperationUpgrade("/posts", "get", add_user_id_param),
],
)
def add_user_id_param(body: dict, params: dict) -> tuple[dict, dict]:
"""Extract user_id from the original path and add as query param."""
# The middleware captures path params before rewrite
if "user_id" in params:
# Already handled
pass
return body, params
Note
For complex path restructuring, you may need both a PathRewrite and an OperationUpgrade to handle parameter
movement.
Versioned OpenAPI Docs
An easy way to validate whether your path rewrites are successfully written in your migration, is by checking the OpenAPI docs for your NinjaAPI.
The versioned OpenAPI schema shows the paths as they existed at each version:
- V1 docs show
/persons/{person_id} - V2 docs show
/people/{person_id}
Clients using the v1 docs will use the old paths, which get rewritten.
Limitations
Path rewrites work for simple URL structure changes. They don't support:
- Query parameter to path conversion (
/items?id=1→/items/1) - Complex routing logic (use operation transformers instead)
- Removing path segments (
/api/v1/items→/items)
For these cases, use Operation Transformers with custom logic.
Next Steps
- Modifying a Schema — Schema transformation examples
- Operation Transformers — Endpoint-specific logic
- Migration Files — Full file format reference