DEV Community

Vincent Tommi
Vincent Tommi

Posted on

Understanding REST Resources: A Guide for Developers with Django ViewSets

In the world of RESTful APIs, the concept of a resource is central to designing intuitive and scalable systems. A well-thought-out resource naming strategy will make your API easy to understand and maintain. By adhering to best practices—using nouns, maintaining consistency, avoiding file extensions, leveraging HTTP methods for actions, and using query parameters—you can create intuitive and maintainable APIs. The Django REST Framework ViewSet examples provided demonstrate how to implement these principles efficiently, ensuring your API is robust and developer-friendly.

What is a Resource?
According to Roy Fielding’s dissertation, a resource is a conceptual mapping to a set of entities, not necessarily the entity itself at any point point in time. In REST, any information that can be named can be a resource is a resource. This could be a document, an image, a service like “today’s weather in Los Angeles,” a collection of resources, or even a non-virtual object like a person. Essentially, anything that can be referenced by a hypertext link is a resource.

Singleton and Collection Resources
Resources can be categorized as collection or singleton:

  • Collection Resource: Represents a group of items, such as “products” in a shopping domain. Example URI: /products.

  • Singleton Resource: Represents a single item within a collection, such as a specific product. Example URI: /products/{productId}.

Collection and Sub-collection Resources

Resources can also contain sub-collections. For instance, in a shopping domain, the reviews for a specific product can be represented as a sub-collection: /products/{productId}/reviews. A single review within the sub-collection would be: /products/{productId}/reviews/{reviewId}.

URIs in REST
Uniform Resource Identifiers (URIs) are used to address resources in REST APIs. Well-designed URIs convey the resource model clearly, making the API intuitive. By combining URIs with HTTP verbs (GET, POST, PUT, DELETE), you adhere to REST standards, ensuring a uniform interface.

Below are a few tips to get you started when creating resource URIs for your new API.

Best Practices

Use Nouns to Represent Resources

RESTful URIs should refer to a resource that is a thing (noun) rather than an action (verb), as nouns have properties, much like resources have attributes. Some examples of resources include:

  • User Accounts

  • Users of the system

  • Internet Devices

Their resource URIs can be designed as follows:

  • /device-management/managed-devices

  • /device-management/managed-devices/{device-id}

  • /user-management/users

  • /user-management/users/{id}

For clarity, resources can be divided into three archetypes: document, collection, and store. Always target one archetype per resource and use its naming convention consistently to avoid hybrid designs.

Document

A document resource is a singular concept, akin to an object instance or database record. It typically includes fields with values and links to related resources. Use singular names for document resources:

http://localhost:8000/device-management/devices/{device-id}
http://localhost:8000/user-management/users/{id}
http://localhost:8000/user-management/users/admin
Enter fullscreen mode Exit fullscreen mode

Collection

A collection resource is a server-managed directory of resources. Clients may propose new resources, but the collection decides whether to create them and assigns their URIs. Use plural names for collection resources:

Collection

A collection resource is a server-managed directory of resources. Clients may propose new resources, but the collection decides whether to create them and assigns their URIs. Use plural names for collection resources:

/device-management/devices
/user-management/users
/user-management/users/{id}/accounts
Enter fullscreen mode Exit fullscreen mode

Store

A store resource is a client-managed repository where clients choose the URIs for stored resources. Use plural names for store resources:

/song-management/users/{id}/playlists

Enter fullscreen mode Exit fullscreen mode

Consistency is the Key

Use consistent resource naming conventions and URI formatting for minimum ambiguity and maximum readability and maintainability. Here are some design hints to achieve consistency:

Use Forward Slash (/) to Indicate Hierarchical Relationships

The forward slash (/) indicates a hierarchical relationship between resources:

/device-management
/device-management/devices
/device-management/devices/{id}
/device-management/devices/{id}/scripts
/device-management/devices/{id}/scripts/{id}
Enter fullscreen mode Exit fullscreen mode

Do Not Use Trailing Forward Slash (/)

Trailing slashes add no semantic value and may cause confusion. Omit them:

http://localhost:8000/device-management/devices  /* Preferred */
http://localhost:8000/device-management/devices/ /* Avoid */
Enter fullscreen mode Exit fullscreen mode

Use Hyphens (-) to Improve Readability

Hyphens improve readability in long path segments:

http://localhost:8000/device-management/devices  /* Readable */
http://localhost:8000/devicemanagement/devices  /* Less readable */
Enter fullscreen mode Exit fullscreen mode

Do Not Use Underscores ( _ )

Underscores can be obscured in some fonts or browsers. Prefer hyphens:

http://localhost:8000/inventory-management/managed-entities/{id}/install-script-location  /* Readable */
http://localhost:8000/inventory-management/managedEntities/{id}/installScriptLocation  /* Less readable */
Enter fullscreen mode Exit fullscreen mode

Use Lowercase Letters in URIs

Lowercase letters ensure consistency, as URIs can be case-sensitive:

http://localhost:8000/my-folder/my-doc  /* Preferred */
http://localhost:8000/My-Folder/my-doc  /* Avoid */
Enter fullscreen mode Exit fullscreen mode

Do Not Use File Extensions

File extensions (e.g., .xml) are unnecessary and should be avoided. Use the Content-Type header to specify the media type:

/device-management/devices  /* Correct */
/device-management/devices.xml  /* Avoid */
Enter fullscreen mode Exit fullscreen mode

Never Use CRUD Function Names in URIs

URIs should identify resources, not actions. Use HTTP methods to indicate CRUD operations. Below is a Django REST Framework example using a ViewSet to implement CRUD operations for a Device resource:

# device_management/models.py
from django.db import models

class Device(models.Model):
    name = models.CharField(max_length=100)
    region = models.CharField(max_length=50)
    brand = models.CharField(max_length=50)
    installation_date = models.DateField()

    def __str__(self):
        return self.name
Enter fullscreen mode Exit fullscreen mode
# device_management/serializers.py
from rest_framework import serializers
from .models import Device

class DeviceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Device
        fields = ['id', 'name', 'region', 'brand', 'installation_date']
Enter fullscreen mode Exit fullscreen mode
# device_management/views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Device
from .serializers import DeviceSerializer

class DeviceViewSet(viewsets.ModelViewSet):
    queryset = Device.objects.all()
    serializer_class = DeviceSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['region', 'brand']

    def get_queryset(self):
        queryset = super().get_queryset()
        sort = self.request.query_params.get('sort')
        if sort == 'installation-date':
            return queryset.order_by('installation_date')
        return queryset
Enter fullscreen mode Exit fullscreen mode
# device_management/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DeviceViewSet

router = DefaultRouter()
router.register(r'device-management/devices', DeviceViewSet, basename='device')

urlpatterns = [
    path('', include(router.urls)),
]
Enter fullscreen mode Exit fullscreen mode

This setup maps to:

  • GET /device-management/devices (list all devices)

  • POST /device-management/devices (create a device)

  • GET /device-management/devices/{id} (retrieve a device)

  • PUT /device-management/devices/{id} (update a device)

  • DELETE /device-management/devices/{id} (delete a device)

Add to your Django settings:

# settings.py
INSTALLED_APPS = [
    ...
    'django_filters',
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

Enter fullscreen mode Exit fullscreen mode

Use Query Component to Filter URI Collection

For sorting, filtering, or paginating collections, use query parameters instead of new APIs:

/device-management/devices
/device-management/devices?region=USA
/device-management/devices?region=USA&brand=XYZ
/device-management/devices?region=USA&brand=XYZ&sort=installation-date
Enter fullscreen mode Exit fullscreen mode

The DeviceViewSet above already includes filtering and sorting capabilities via query parameters, leveraging django-filter for fields like region and brand, and custom sorting for installation-date.

Do Not Use Verbs in the URI

Including verbs in URIs, like /scripts/{id}/execute, suggests an RPC-style call, not REST. Instead, treat actions as resources. For example, to execute a script, create a resource representing the script’s status and use a POST request:

# device_management/models.py
from django.db import models

class Script(models.Model):
    device = models.ForeignKey(Device, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    status = models.CharField(max_length=50, default='pending')

    def __str__(self):
        return self.name
Enter fullscreen mode Exit fullscreen mode
# device_management/serializers.py
from rest_framework import serializers
from .models import Script

class ScriptStatusSerializer(serializers.ModelSerializer):
    action = serializers.CharField(write_only=True)

    class Meta:
        model = Script
        fields = ['id', 'device', 'name', 'status', 'action']
Enter fullscreen mode Exit fullscreen mode
# device_management/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Script
from .serializers import ScriptStatusSerializer

class ScriptViewSet(viewsets.ModelViewSet):
    queryset = Script.objects.all()
    serializer_class = ScriptStatusSerializer

    @action(detail=True, methods=['post'], url_path='status')
    def update_status(self, request, device_id=None, pk=None):
        try:
            script = Script.objects.get(id=pk, device_id=device_id)
        except Script.DoesNotExist:
            return Response({"error": "Script not found"}, status=status.HTTP_404_NOT_FOUND)

        serializer = self.get_serializer(script, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        if serializer.validated_data.get('action') == 'execute':
            script.status = 'executing'
            script.save()
            return Response({"id": script.id, "status": script.status}, status=status.HTTP_200_OK)
        return Response({"error": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST)
Enter fullscreen mode Exit fullscreen mode
# device_management/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DeviceViewSet, ScriptViewSet

router = DefaultRouter()
router.register(r'device-management/devices', DeviceViewSet, basename='device')
router.register(r'device-management/devices/(?P<device_id>[^/.]+)/scripts', ScriptViewSet, basename='script')

urlpatterns = [
    path('', include(router.urls)),
]
Enter fullscreen mode Exit fullscreen mode

This setup uses a ViewSet with a custom status action to handle script execution via a POST request to /device-management/devices/{device_id}/scripts/{id}/status, keeping the URI noun-based.

Conclusion

In conclusion, resources in REST are primarily nouns (objects), and we should avoid deviating from this principle. Using verbs in URIs often leads to RPC-style calls, which are not RESTful. By adhering to best practices—using nouns, maintaining consistency, avoiding file extensions, leveraging HTTP methods for actions, and using query parameters for filtering—you can create intuitive and maintainable APIs. The Django REST Framework ViewSet examples provided demonstrate how to implement these principles efficiently, ensuring your API is robust and developer-friendly.

Top comments (0)