Experimenting with
File-based Routing
in Django


Sage Abdullah

Wagtail Developer at Torchbox

@laymonage
  • File-based routing? ๐Ÿค”
  • How it works in other web frameworks ๐Ÿง
  • How it may be implemented in a Django project ๐Ÿงช
  • Possible improvements and their challenges ๐Ÿšง
  • How you can use it in your project โœ…

2020

2021

Next.js logo
Django logo
โžก๏ธ
Next.js logo
urls.js? ๐Ÿคท

File-based routing

File-based routing

Instead of defining routes in your code...

File-based routing

Instead of defining routes in your code...

...routes are defined by your code.

File-based routing

Instead of defining routes in your code...

...routes are defined by your code structure.

How does it work?

Next.js logo
Next.js logo
Myproject structure, with 'pages' directory containing 'index.js' and a subdirectory 'blog' containing 'index.js'
Next.js logo
Myproject structure, with 'pages' directory containing 'index.js' and a subdirectory 'blog' containing 'index.js'




/blog
/
            
Next.js logo

              pages/index.jsx       โ†’  /
              pages/blog/index.jsx  โ†’  /blog
            
Next.js logo

              pages/index.jsx       โ†’  /
              pages/blog/index.jsx  โ†’  /blog
            

How is this possible?

JavaScript exports


              // pages/index.jsx

              export function HomePage() {
                  return 

Hello, world!

; }

JavaScript exports


              // pages/index.jsx

              export default function HomePage() {
                  return 

Hello, world!

; }
Next.js logo

              pages/
              โ”œโ”€โ”€ blog/
              โ”‚   โ”œโ”€โ”€ index.jsx            โ†’  /blog
              โ”‚   โ””โ”€โ”€ first-post.jsx       โ†’  /blog/first-post
              โ”œโ”€โ”€ settings/
              โ”‚   โ””โ”€โ”€ account/
              โ”‚       โ””โ”€โ”€ password.jsx     โ†’  /settings/account/password
              โ””โ”€โ”€ index.jsx                โ†’  /
            
Next.js logo

              pages/
              โ”œโ”€โ”€ blog/
              โ”‚   โ”œโ”€โ”€ index.jsx            โ†’  /blog
              โ”‚   โ””โ”€โ”€ [slug].jsx           โ†’  /blog/:slug
              โ”œโ”€โ”€ profile/
              โ”‚   โ””โ”€โ”€ [username]/
              โ”‚       โ”œโ”€โ”€ index.jsx        โ†’  /profile/:username
              โ”‚       โ””โ”€โ”€ likes.jsx        โ†’  /profile/:username/likes
              โ”œโ”€โ”€ settings/
              โ”‚   โ””โ”€โ”€ account/
              โ”‚       โ””โ”€โ”€ password.jsx     โ†’  /settings/account/password
              โ”œโ”€โ”€ unknown/
              โ”‚   โ””โ”€โ”€ [...all].jsx         โ†’  /unknown/*
              โ””โ”€โ”€ index.jsx                โ†’  /
            
Remix logo

              root.jsx                     โ†’  /
              routes/
              โ”œโ”€โ”€ blog/
              โ”‚   โ”œโ”€โ”€ index.jsx            โ†’  /blog
              โ”‚   โ””โ”€โ”€ $slug.jsx            โ†’  /blog/:slug
              โ”œโ”€โ”€ profile/
              โ”‚   โ”œโ”€โ”€ $username/
              โ”‚   โ”‚   โ””โ”€โ”€ likes.jsx        โ†’  /profile/:username/likes
              โ”‚   โ””โ”€โ”€ $username.jsx        โ†’  /profile/:username
              โ”œโ”€โ”€ settings/
              โ”‚   โ””โ”€โ”€ account/
              โ”‚       โ””โ”€โ”€ password.jsx     โ†’  /settings/account/password
              โ””โ”€โ”€ unknown/
                  โ””โ”€โ”€ $.jsx                โ†’  /unknown/*
            
NuxtJS logo

              pages/
              โ”œโ”€โ”€ blog/
              โ”‚   โ”œโ”€โ”€ index.vue            โ†’  /blog
              โ”‚   โ””โ”€โ”€ _slug.vue            โ†’  /blog/:slug
              โ”œโ”€โ”€ profile/
              โ”‚   โ””โ”€โ”€ _username/
              โ”‚       โ”œโ”€โ”€ index.vue        โ†’  /profile/:username
              โ”‚       โ””โ”€โ”€ likes.vue        โ†’  /profile/:username/likes
              โ”œโ”€โ”€ settings/
              โ”‚   โ””โ”€โ”€ account/
              โ”‚       โ””โ”€โ”€ password.vue     โ†’  /settings/account/password
              โ”œโ”€โ”€ unknown/
              โ”‚   โ””โ”€โ”€ _.vue                โ†’  /unknown/*
              โ””โ”€โ”€ index.vue                โ†’  /
            
SvelteKit logo

              routes/                      โ†’  /
              โ”œโ”€โ”€ blog/                    โ†’  /blog
              โ”‚   โ”œโ”€โ”€ +page.svelte
              โ”‚   โ””โ”€โ”€ [slug]/              โ†’  /blog/:slug
              โ”‚       โ””โ”€โ”€ +page.svelte
              โ”œโ”€โ”€ profile/
              โ”‚   โ””โ”€โ”€ [username]/          โ†’  /profile/:username
              โ”‚       โ”œโ”€โ”€ +page.svelte
              โ”‚       โ””โ”€โ”€ likes/           โ†’  /profile/:username/likes
              โ”‚           โ””โ”€โ”€ +page.svelte
              โ”œโ”€โ”€ settings/
              โ”‚   โ””โ”€โ”€ account/
              โ”‚       โ””โ”€โ”€ password/         โ†’  /settings/account/password
              โ”‚           โ””โ”€โ”€ +page.svelte
              โ”œโ”€โ”€ unknown/
              โ”‚   โ””โ”€โ”€ [...rest]/            โ†’  /unknown/*
              โ”‚       โ”œโ”€โ”€ +error.svelte
              โ”‚       โ””โ”€โ”€ +page.svelte
              โ””โ”€โ”€ +page.svelte
            
Gatsby logo

              pages/
              โ”œโ”€โ”€ blog/
              โ”‚   โ””โ”€โ”€ {MarkdownRemark.parent__(File)__name}.js  โ†’  /blog/:filename
              โ”œโ”€โ”€ products/
              โ”‚   โ””โ”€โ”€ {Product.category}/
              โ”‚       โ””โ”€โ”€ {Product.fields__sku}.js              โ†’  /products/:category/:sku
              โ”œโ”€โ”€ profile/
              โ”‚   โ”œโ”€โ”€ [id]/
              โ”‚   โ”‚   โ””โ”€โ”€ group/
              โ”‚   โ”‚       โ””โ”€โ”€ [groupId].js                      โ†’  /profile/:id/group/:groupId
              โ”‚   โ””โ”€โ”€ [id].js                                   โ†’  /profile/:id
              โ””โ”€โ”€ unknown/
                  โ””โ”€โ”€ [...rest].js                              โ†’  /unknown/*
            

Why is it so popular?

Why is it so popular?

code structure โ†”๏ธ url structure

Why is it so popular?


              import BlogPost from 'pages/blog/[slug]';
            

How can we have this in Django?

File structure

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

                myapp/views/
                โ”œโ”€โ”€ about.py
                โ”œโ”€โ”€ index.py
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].py
                โ”‚   โ””โ”€โ”€ index.py
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].py
                    โ””โ”€โ”€ index.py
              

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

                myapp/views/
                โ”œโ”€โ”€ about.py
                โ”œโ”€โ”€ __init__.py
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].py
                โ”‚   โ””โ”€โ”€ __init__.py
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].py
                    โ””โ”€โ”€ __init__.py
              

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

                myapp/views/
                โ”œโ”€โ”€ about.py
                โ”œโ”€โ”€ __init__.py
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ <slug:slug>.py
                โ”‚   โ””โ”€โ”€ __init__.py
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ <int:id>.py
                    โ””โ”€โ”€ __init__.py
              

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

                myapp/views/
                โ”œโ”€โ”€ about.py
                โ”œโ”€โ”€ __init__.py
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ __slug__slug__.py
                โ”‚   โ””โ”€โ”€ __init__.py
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ __int__id__.py
                    โ””โ”€โ”€ __init__.py
              

File structure


                pages/
                โ”œโ”€โ”€ about.jsx
                โ”œโ”€โ”€ index.jsx
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ [slug].jsx
                โ”‚   โ””โ”€โ”€ index.jsx
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ [id].jsx
                    โ””โ”€โ”€ index.jsx
              

                myapp/views/
                โ”œโ”€โ”€ about.py
                โ”œโ”€โ”€ __init__.py
                โ”œโ”€โ”€ posts/
                โ”‚   โ”œโ”€โ”€ slug.py
                โ”‚   โ””โ”€โ”€ __init__.py
                โ””โ”€โ”€ projects/
                    โ”œโ”€โ”€ id.py
                    โ””โ”€โ”€ __init__.py
              

Views

Views


                export default function HomePage() {
                    return 

Hello, world!

; }

Views


              export default function HomePage() {
                  return 

Hello, world!

; }

                def default(request):
                    return HttpResponse("Hello, world!")
              

Views


              export default function HomePage() {
                  return 

Hello, world!

; }

                def view(request):
                    return HttpResponse("Hello, world!")
              

Views


              export default function HomePage() {
                  return 

Hello, world!

; }

                def dispatch(request):
                    return HttpResponse("Hello, world!")
              

Hooking it into urls.py

Hooking it into urls.py


              urlpatterns = [
                  fs_paths("myapp.views"),
                  # or fs_paths("myapp/views") ?
                  # or *fs_paths("myapp/views") ?
              ]
            

fs_paths

fs_paths


              def fs_paths(module_path):
                  ...
            

fs_paths


              def fs_paths(module_path):
                  # 1. Get the module object from module_path (dynamic import)
            

fs_paths


              def fs_paths(module_path):
                  # 1. Get the module object from module_path (dynamic import)
                  # 2. Use getattr(module, "dispatch") to get the view function
            

fs_paths


              def fs_paths(module_path):
                  # 1. Get the module object from module_path (dynamic import)
                  # 2. Use getattr(module, "dispatch") to get the view function
                  # 3. Construct path(route, view)
            

fs_paths


              def fs_paths(module_path):
                  # 1. Get the module object from module_path (dynamic import)
                  # 2. Use getattr(module, "dispatch") to get the view function
                  # 3. Construct path(route, view)
                  # 4. Repeat for all submodules
            

fs_paths


              from importlib import import_module

              def fs_paths(module_path):
                  module = import_module(module_path)
                  # 2. Use getattr(module, "dispatch") to get the view function
                  # 3. Construct path(route, view)
                  # 4. Repeat for all submodules
            

fs_paths


              from importlib import import_module

              def fs_paths(module_path):
                  module = import_module(module_path)
                  view = getattr(module, "dispatch")
                  # 3. Construct path(route, view)
                  # 4. Repeat for all submodules
            

fs_paths


              from importlib import import_module
              from django.urls import path

              def fs_paths(module_path):
                  module = import_module(module_path)
                  view = getattr(module, "dispatch")
                  return path("", view, name="index")
                  # 4. Repeat for all submodules
            

fs_paths


              from importlib import import_module
              from django.urls import path

              def fs_paths(module_path):
                  result = []
                  module = import_module(module_path)
                  view = getattr(module, "dispatch", None)
                  if callable(view):
                      result.append(path("", view, name="index"))

                  return path("", include((result, module_path)))

            

fs_paths


              from importlib import import_module
              from django.urls import path

              def fs_paths(module_path, namespace=None):
                  result = []
                  module = import_module(module_path)
                  view = getattr(module, "dispatch", None)
                  if callable(view):
                      result.append(path("", view, name="index"))

                  if not namespace:
                      namespace = module_path
                  return path("", include((result, namespace)))

            

fs_paths


              from pkgutil import walk_packages
              from importlib import import_module
              from django.urls import path

              def fs_paths(module_path, namespace=None):
                  result = []
                  module = import_module(module_path)
                  view = getattr(module, "dispatch", None)
                  if callable(view):
                      result.append(path("", view, name="index"))

                  if not namespace:
                      namespace = module_path

                  prefix = f"{module_path}.")
                  for pkg in walk_packages(module.__path__, prefix=prefix):
                      finder, name, _ = pkg  # pkgutil.ModuleInfo
                      module = finder.find_module(name).load_module(name)
                      view = getattr(module, "dispatch", None)
                      route_name = name[len(prefix):]
                      route = route_name.replace('.', '/')
                      if callable(view):
                          result.append(path(route, view, name=route_name))

                  result.sort(key=lambda x: str(x.pattern), reverse=True)

                  return path("", include((result, namespace)))

            

urls.py


              urlpatterns = [
                  fs_paths("myapp.views", "myapp"),
              ]
            

What about path parameters?

What about path parameters?

It "just works โ„ข๏ธ"

What about path parameters?


              # myapp/views/posts/slug.py
              path = "posts/<slug:slug>"

              def dispatch(request, slug):
                  ...
            

What about path parameters?


              # myapp/views/projects/id.py
              path = "projects/<int:id>"

              def dispatch(request, id):
                  ...
            

What about path parameters?


              route = route_name.replace('.', '/')
              if callable(view):
                  result.append(path(route, view))
            

What about path parameters?


              route = getattr(module, "path", route_name.replace('.', '/'))
              if callable(view):
                  result.append(path(route, view, name=route_name))
            

What about path parameters?


              # myapp/views/projects/id.py
              path = "<int:id>"

              def dispatch(request, id):
                  ...
            

Why dispatch?

Why dispatch?


              def dispatch(request, *args, **kwargs):
                  ...
            

Why dispatch?


              def get(request, *args, **kwargs):
                  ...

              def post(request, *args, **kwargs):
                  ...

              ...
            

Regex paths?

Regex paths?


              re_path = r"(?P<year>[0-9]{4})"

              def dispatch(request, *args, **kwargs):
                  ...
            

How can you use this?

How can you use this?

laymonage/django-fs-paths repository on GitHub

github.com/laymonage/django-fs-paths

How can you use this?

jerivas/django-file-router repository on GitHub

github.com/jerivas/django-file-router

Thank you!

Thank you!

{
  "name": "Sage Abdullah",
  "username": "laymonage",
  "slides": {
    "hosted": "https://slides.laymonage.com/fs-paths",
    "source": "https://github.com/laymonage/slides-fs-paths"
  },
  "project": "https://github.com/laymonage/django-fs-paths",
  "alternative": "https://github.com/jerivas/django-file-router",
  "misc": {
    "jobs": "https://torchbox.com/careers",
    "wagtail": "https://wagtail.org"
  }
}