Search code examples
djangourldjango-rest-frameworkslug

Using Django, should I implement two different url mapping logics in my REST API and web application, and how?


I have a Book model an instance of which several instances of a Chapter model can be linked through Foreign Key:

class Book(models.Model):
    pass

class Chapter(models.Model):
    book = models.ForeignKey(to=Book, ...)
    
    class Meta:
        order_with_respect_to = "book"

I decided to use Django both for the RESTful API, using Django Rest Framework, and for the web application, using Django Template. I want them separate as the way should be open for another potential application to consume the API.

For several reasons including administration purposes, the API calls for a url mapping logic of this kind:

mylibrary.org/api/books/
mylibrary.org/api/books/<book_id>/
mylibrary.org/api/chapters/
mylibrary.org/api/chapters/<chapter_id>/

For my web application, however, I would like the user to access the books and their contents through this url mapping logic:

mylibrary.org/books/
mylibrary.org/books/<book_id>-esthetic-slug/
mylibrary.org/books/<book_id>-esthetic-slug/chapter_<chapter_order>/

The idea is the router to fetch the book from the <book_id>, whatever the slug might be, and the chapter according to its order and not its ID.

Now I need some advice if this is desirable at all or if I am bound to encounter obstacles. For example, how and where should the web app's <book_id>/<chapter_order> be "translated" into the API's <chapter_id>? Or if I want the web app's list of books to offer automatically generated slugged links to the books, should it be done at the API or the web app level?

I'm pretty new to Django / DRF and web development in general.


Solution

  • The double URLs are not a problem at all. It is used whenever the program renders views and has an API (perhaps for a mobile app) as well. What I usually do is have the id field be in the URL of the 'web' pages too, it is simpler for me to keep track of. However if you want to have a 'nice' slug in the URL, try this:

    """
    models.py
    """
    
    import uuid
    
    from django.db import models
    
    
    class Book(models.Model):
        id = models.UUIDField(primary_key=True, null=False, editable=False, default=uuid.uuid4)
        slug = models.SlugField(max_length=50)
    
    class Chapter(models.Model):
        id = models.UUIDField(primary_key=True, null=False, editable=False, default=uuid.uuid4)
        book = models.ForeignKey(to=Book, ...)
        sort = models.PositiveIntegerField()
        
        class Meta:
            order_with_respect_to = "book"
            unique_together = [("book", "sort"), ]  # so that there is never two chapters with the same number
    
    """
    urls.py
    """
    
    from django.urls import path
    
    from . import apiviews, views
    
    urlpatterns = [
        # 'web' URLs
        path("books/", views.books, "book_list"),  # mylibrary.org/books/
        path("books/<str:book_slug>", views.book, "book"),  # mylibrary.org/books/harry-potter-and-the-prisoner-of-azkaban
        path("books/<str:book_slug>/chapter_<int:chapter>", views.chapter, "chapter"),  # mylibrary.org/books/harry-potter-and-the-prisoner-of-azkaban/chapter_2
        # API URLs
        path("api/books/", apiviews.books, "api_book_list"),  # mylibrary.org/api/books/
        path("api/books/<uuid:id>", apiviews.book, "api_book"),  # mylibrary.org/api/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459
        path("api/chapters/", apiviews.chapters, "api_chapters"),  # mylibrary.org/api/chapters/
        path("api/chapters/<uuid:id>", apiviews.chapter, "api_chapter"),  # mylibrary.org/api/chapters/c76a89ae-ad73-4752-991c-8a82e47d3307
    ]
    
    """
    views.py
    """
    
    from .models import Book, Chapter
    
    
    def books(request):
        book_list = Book.objects.all()
    
    
    def book(request, book_slug)
        try:
            book = Book.objects.get(slug=book_slug)
        except Book.DoesNotExist:
            pass
    
    
    def chapter(request, book_slug, chapter)
        try:
            book = Book.objects.get(slug=book_slug)
            chapter = book.chapter_set.get(sort=chapter)
        except Book.DoesNotExist:
            pass
        except Chapter.DoesNotExist:
            pass
    
    """
    apiviews.py
    """
    
    from .models import Book, Chapter
    
    
    def books(request):
        book_list = Book.objects.all()
    
    
    def book(request, id)
        try:
            book = Book.objects.get(id=id)
        except Book.DoesNotExist:
            pass
    
    
    def chapters(request):
        chapter_list = Chapter.objects.all()
    
    
    def chapter(request, id)
        try:
            chapter = Chapter.objects.get(id=id)
        except Chapter.DoesNotExist:
            pass
    

    These files reflect how you suggested the URLs. I personally (and I don't say that is the correct way) would have the API views set up like this:

    mylibrary.org/books/
    mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459
    mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/chapters/
    mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/chapters/3
    # and then for example:
    mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/comments/
    mylibrary.org/books/b21a0af6-4e6c-4c67-b44d-50f8b52cd459/comments/295
    

    I find this structure more comprehensive but I can't see any pros or cons against your version. Both work just as fine and there is a good reasoning behind both of them. One being "API is just a way to access the database objects and so I should be able to pull an arbitrary chapter if I know the ID", the other "I have books and books have chapters, so a chapter is a sub-thing of books".