djangodjango-modelsdjango-rest-frameworkdjango-views

Django REST API endpoints URL paths


I have a Django 4.2 app with Postgres DB and REST API. My urls.py contains this path in urlpatterns:

path('create/<int:pk>/<str:name>/', ComponentCreate.as_view(), name='create-component')

ComponentCreate in views.py relates to a simple DB table (component) with id as integer primary key and name as the only other column.

views.py has:

class ComponentCreate(generics.CreateAPIView):
    queryset = Component.objects.all(),
    serializer_class = ComponentSerializer
    lookup_field = "id"

models.py has:

class Component(models.Model):
    id = models.IntegerField(primary_key=True)
    name = models.CharField(max_length=255, blank=True, null=True)
    class Meta:
        managed = True
        db_table = 'component'

serializers.py has:

class ComponentSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField()
    name = serializers.CharField()

    class Meta:
        model = Component
        fields = ('id', 'name')

I am trying to use the API to add a row to the component table using e.g. as below (where app is called SystemTestDB):

curl -X POST http://127.0.0.1:8000/SystemTestDB/create/2/whatever/

However this fails with response:

{"id":["This field is required."],"name":["This field is required."]}

I have other endpoints which do work correctly e.g. with path:

path('delete/<int:pk>/', ComponentDelete.as_view(), name='delete-component')

In that case evidently id is being passed via int:pk, whilst in the failing case neither id nor name are set in the Request.

Have I set the URL path incorrectly, or is there something wrong with model/view/serializer ?


Solution

  • Your proposed setup isn't idiomatic for passing fields in a request. With djangorestframework the URL isn't for persisting dynamic data, it's for telling the server which resources you want to access.

    TL;DR: The following block of text is educational. Code is at the bottom.

    For the delete path, your app behaves correctly because that is the correct way to pass such a request - you want to access the resource given by id (to delete it), so the URL is the appropriate place to put it.

    For create paths, the resource doesn't exist yet so it doesn't have an id - and as such, there isn't a case for you telling the server that you want to access that resource. You want the server to build that resource.

    And the appropriate place to put information necessary to build that specific resource is in the request body, not the URL.

    The same is true for update (or in HTML, PATCH/PUT), except in those cases the resource does exist, so you will be passing the id as part of the URL - but the data with which to update that resource should still be passed in the request body.

    You can make a setup with passing variable data in the URL work - it's just not usual, nor recommended. But if you aren't deliberately choosing that pattern for very specific architectural reasons - then it's almost universally better to stick with a recommended pattern.

    The "intended" way to do things in djangorestframework

    # models.py
    
    class Component(models.Model):
        # Instead of IntegerField, use (Big)AutoField to let the database handle 
        # the resource's primary key (meaning you don't have to pass it manually)
        id = models.BigAutoField(primary_key=True)  
    
    # serializers.py
    
    class ComponentSerializer(serializers.ModelSerializer):
        # ModelSerializer fetches the fields from the model
        # Only define them here if you need to override the default behavior
        #    (which your use-case doesn't seem like you do)
        class Meta:
            model = Component
            fields = ('id', 'name')
    
    class ComponentCreate(generics.CreateAPIView):
        queryset = Component.objects.all()
        serializer_class = ComponentSerializer
        # You don't need lookup_field in this view, CreateAPIView doesn't perform any lookups
    
    # urls.py
    
    path('create/', ComponentCreate.as_view(), name='create-component')
    
    # request
    
    curl -X POST http://127.0.0.1:8000/SystemTestDB/create/ -H "Content-Type: application/json" -d '{"name": "whatever"}'