Library / SDK
alanjds/drf-nested-routers avatar
alanjds/drf-nested-routers

drf-nested-routers: nesting Django REST Framework routes without hand-written URLconf

Nested Routers for Django Rest Framework

1,798 stars171 forksPythonApache-2.0

At a glance

What is it?
drf-nested-routers builds child URL trees on top of an existing DRF router so a nameserver, recipient or line item is only reachable through its parent. It ships no models or signals, and the README itself still carries a work in progress warning.
Who is it for?
Adopt drf-nested-routers if your API already runs on DRF and you need child collections that must be scoped to a parent, and be aware you still have to filter every queryset by the parent kwarg yourself. Do not reach for it if you want automatic permission or object scoping, or if you are on Python below 3.10, where pyproject.toml declares no support.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 18, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A nameserver cannot be fetched without its domain

The package provides routers for creating nested resources in Django REST Framework. The example in the README is a DNS service: many domains, many nameservers each, and a nameserver that has no meaning detached from its domain. The URL tree the package targets looks like this.

code
/domains/ <- Domains list
/domains/{pk}/ <- One domain, from {pk}
/domains/{domain_pk}/nameservers/ <- Nameservers of domain from {domain_pk}
/domains/{domain_pk}/nameservers/{pk} <- Specific nameserver from {pk}, of domain from {domain_pk}

The reader this is for is a Django developer who already has viewsets and serializers and does not want to hand-write a URLconf for every child collection. Nothing here replaces DRF, and nothing here replaces permissions or filtering. It only supplies the routing layer plus a serializer field and a mixin for nested hyperlinks.

NestedSimpleRouter wraps the parent router instead of replacing it

The mechanism is a subclass that takes your existing router, the parent prefix, and a lookup name. It then registers a child viewset on itself and produces URLs whose paths are prefixed by the parent segment.

python
# urls.py
from rest_framework_nested import routers
from views import DomainViewSet, NameserverViewSet
(...)

router = routers.SimpleRouter()
router.register(r'domains', DomainViewSet)

domains_router = routers.NestedSimpleRouter(router, r'domains', lookup='domain')
domains_router.register(r'nameservers', NameserverViewSet, basename='domain-nameservers')

The `lookup='domain'` argument is what determines the kwarg name, and therefore the URL segment: it produces `domain_pk` rather than the plain `pk`. Both routers are then included side by side.

python
urlpatterns = [
    path(r'', include(router.urls)),
    path(r'', include(domains_router.urls)),
]

The `basename` argument is optional and only needed when the same viewset is registered more than once, since DRF derives its URL names from the basename. Note that the nested router is a child of the parent router object, not a standalone router: pass the parent router instance as the first argument.

The router generates the route, your viewset supplies the filter

This is the part that catches people. drf-nested-routers does not scope anything for you. It creates the URL and injects `domain_pk` into `self.kwargs`, and then filtering is your code. For an ORM backed resource the README shows a `get_queryset` that reads the kwarg directly.

python
class NameserverViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Nameserver.objects.filter(domain=self.kwargs['domain_pk'])

For non-ORM resources the same kwarg is accepted by the action methods.

python
class NameserverViewSet(viewsets.ViewSet):
    def list(self, request, domain_pk=None):
        nameservers = self.queryset.filter(domain=domain_pk)
        (...)
        return Response([...])

The failure mode is straightforward: if you register the child viewset and forget the filter, every child object in the database becomes reachable under every parent, because the router will happily route `/domains/1/nameservers/` and `/domains/2/nameservers/` to the same unfiltered queryset. There is no built-in guard against that.

Nested hyperlinks need a parent_lookup_kwargs mapping

If your serializers are hyperlinked, the default DRF related fields cannot build a child URL, because the child URL needs two pieces of identity. The package supplies `NestedHyperlinkedRelatedField` and `NestedHyperlinkedModelSerializer` for this, and both hinge on a `parent_lookup_kwargs` dict that tells the serializer which model field maps to which URL kwarg.

python
from rest_framework_nested.relations import NestedHyperlinkedRelatedField

class DomainSerializer(HyperlinkedModelSerializer):
    class Meta:
        model = Domain

    nameservers = NestedHyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='domain-nameservers-detail',
        parent_lookup_kwargs={'domain_pk': 'domain__pk'}
    )

The keys are URL kwargs, the values are ORM lookups on the child model, which is why the key has one underscore and the value has two. A variation uses `HyperlinkedIdentityField` with `lookup_url_kwarg='domain_pk'` instead, but that variant calls `.get(domain_pk=domain_pk)` on the queryset rather than filtering, so it suits a field that points at one identity endpoint. If you want to trim which fields appear on the nested relation, the serializer subclass form is the documented route.

Nesting three levels deep is the same call repeated

The README shows a client, maildrop, recipient chain, and the point is that depth costs nothing extra: you chain routers, each one wrapping the previous.

python
router = DefaultRouter()
router.register(r'clients', ClientViewSet, basename='clients')

client_router = routers.NestedSimpleRouter(router, r'clients', lookup='client')
client_router.register(r'maildrops', MailDropViewSet, basename='maildrops')

maildrops_router = routers.NestedSimpleRouter(client_router, r'maildrops', lookup='maildrop')
maildrops_router.register(r'recipients', MailRecipientViewSet, basename='recipients')

The third router receives `client_router`, not `router`, so URLs accumulate both parent kwargs. The cost shows up in the viewsets instead: a three level chain means a viewset for recipients must filter on both `client_pk` and `maildrop_pk`, and every kwarg you fail to read is another kwarg a client can tamper with.

Installation is one pip command and no settings entry

There is no Django app to register. The README states plainly that the library contains no app, signal or model, so nothing goes into `settings.py` in `INSTALLED_APPS`.

bash
pip install drf-nested-routers

Package metadata in pyproject.toml requires Python 3.10 or newer and pulls in `Django>=4.2` and `djangorestframework>=3.15.0`. The README's compatibility table is narrower in one respect and wider in another: it lists Python 3.9 through 3.13, Django 4.2 through 5.2 and DRF 3.14 through 3.16, while noting that since release 0.94.2 nothing below Python 3.8 is tested on CI, and since 0.92.1 nothing from Python 2.7 to 3.5, Django 1.11 to 2.1 or DRF 3.6 to 3.10 is tested. The pyproject classifiers cover Python 3.10 to 3.13 and Django 4.2 to 5.2 only, so the 3.9 and DRF 3.14 rows are aspirational rather than declared. The package ships a `py.typed` marker and is typed on mypy.ini terms, which is unusual for a library of this size.

The author's own warning, and how to weigh it

The first line of the README is not marketing. It reads: "This is a work in progress. It 'works for me' at www.apiregistro.com.br, but I cannot warranty that it fully 'works everywhere' yet." That sentence is worth reading before you adopt the package, and it is still there alongside a build badge and a Gitter link.

Maintenance signals are otherwise healthy. The last push was on 2026-10-04, and releases v0.95.3 and v0.95.2 both landed on 2026-07-23, after a long gap back to v0.90.0 on 2017-03-12. The gap between 2017 and 2026 tells you the project sat quiet for years and then resumed, so your upgrade notes should assume a jump rather than an increment. The licence is Apache-2.0, permissive and fine for commercial use, and the pyproject metadata declares the same identifier. The version number itself is unconventional, jumping from 0.90 to 0.92 to 0.94 to 0.95, which tells you the project does not follow semver signals about API stability.

Hand-written URLs are the honest alternative

Object-level permission checks still need to be configured separately. If a user can request `/domains/999/nameservers/`, your queryset filter and your permission class both have to reject it.

Editorial conclusion

Adopt drf-nested-routers if your API already runs on DRF and you need child collections that must be scoped to a parent, and be aware you still have to filter every queryset by the parent kwarg yourself. Do not reach for it if you want automatic permission or object scoping, or if you are on Python below 3.10, where pyproject.toml declares no support. Before upgrading, check that your viewset's get_queryset handles every kwarg your nested URLs inject, because the router generates those routes whether or not your filter code accounts for them.

Frequently asked questions

Does drf-nested-routers need to be added to INSTALLED_APPS?

No. The README states the library contains no app, signal or model, so there is nothing to register in settings.py.

Which Python and Django versions does drf-nested-routers support?

pyproject.toml requires Python 3.10 or newer with Django>=4.2 and djangorestframework>=3.15.0, and its classifiers list Python 3.10 to 3.13 and Django 4.2 to 5.2. The README also lists Python 3.9 and DRF 3.14 as working but notes that since release 0.94.2 only Python 3.8 and newer are tested on CI.

Why does my nested viewset return every child object instead of only the parent's?

The router only builds URLs and injects the parent kwarg; it does not filter. You must read that kwarg yourself, for example `return Nameserver.objects.filter(domain=self.kwargs['domain_pk'])` in `get_queryset`.

How do I nest a third level of resources?

Chain the routers, passing the previous nested router as the parent. Each NestedSimpleRouter adds its own prefix and lookup kwarg, so the resulting URLs read /clients/{client_pk}/maildrops/{maildrop_pk}/recipients/.

How do I build hyperlinks to nested resources in a serializer?

Use NestedHyperlinkedRelatedField or NestedHyperlinkedModelSerializer from rest_framework_nested and set parent_lookup_kwargs, for example `{'domain_pk': 'domain__pk'}`, where the key is the URL kwarg and the value is the ORM lookup on the child model.

Official sources

  1. alanjds/drf-nested-routers on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/alanjds-drf-nested-routers.svg)](https://hysenlabs.com/projects/alanjds-drf-nested-routers)