APIs - CameronAuler/python-devops GitHub Wiki

RESTful vs RESTless APIs

When designing APIs, it's essential to follow architectural principles that ensure scalability, maintainability, and efficiency. This is where the REST (Representational State Transfer) architecture comes in. However, not all APIs that claim to be "REST" fully adhere to REST principlesβ€”some are RESTful, while others are RESTless.

  • βœ… Scalability: Follows industry standards and can handle high traffic.
  • βœ… Interoperability: Can be used by multiple clients (web, mobile, CLI).
  • βœ… Security: Uses proper authentication (JWT, OAuth, API keys).
  • βœ… Maintainability: Easier to update and extend without breaking compatibility.

What is a RESTful API?

A RESTful API adheres to REST architectural constraints, meaning:

  • βœ… Uses correct HTTP methods for different operations:
    • GET β†’ Retrieve data
    • POST β†’ Create a new resource
    • PUT/PATCH β†’ Update an existing resource
    • DELETE β†’ Remove a resource
  • βœ… Uses resource-based URLs rather than action-based URLs:
    • βœ… GET /users/ (fetch all users)
    • βœ… GET /users/123/ (fetch a specific user)
    • βœ… POST /users/ (create a new user)
  • βœ… Stateless communication – Each request must contain all necessary information, and the server does not store client state between requests.
  • βœ… Follows client-server separation – The API and the client consuming it are independent, allowing different clients (web, mobile, CLI) to interact with the API without modifications.
  • βœ… Uses JSON (or XML) as the standard response format, ensuring interoperability.
  • βœ… Supports HATEOAS (Hypermedia as the Engine of Application State) – API responses include links to related resources for navigation.

RESTful API Example

Define the model (model.py)

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=100, unique=True)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.username

Create a Serializer (serializers.py)

from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"

Define API Views (views.py)

from rest_framework import generics
from .models import User
from .serializers import UserSerializer

# Endpoint: GET /users/  | POST /users/
class UserListCreateView(generics.ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# Endpoint: GET /users/<id>/  | PUT/PATCH /users/<id>/  | DELETE /users/<id>/
class UserDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

Define URLs (urls.py)

from django.urls import path
from .views import UserListCreateView, UserDetailView

urlpatterns = [
    path("users/", UserListCreateView.as_view(), name="user-list-create"),
    path("users/<int:pk>/", UserDetailView.as_view(), name="user-detail"),
]
  • βœ… Now, we have a fully RESTful API with:
    • GET /users/ β†’ Fetch all users
    • POST /users/ β†’ Create a new user
    • GET /users/123/ β†’ Fetch a single user
    • PUT/PATCH /users/123/ β†’ Update user info
    • DELETE /users/123/ β†’ Remove a user

What is a RESTless API?

A RESTless API does not fully follow REST principles, meaning:

  • ❌ Uses incorrect HTTP methods (e.g., using POST for data retrieval).
  • ❌ Uses action-based URLs instead of resource-based URLs:
    • ❌ POST /getUserData instead of GET /users/123/
    • ❌ POST /updateUser instead of PUT /users/123/
  • ❌ Not stateless – Some RESTless APIs rely on server-side sessions instead of passing authentication tokens.
  • ❌ Inconsistent data formats – Responses may not follow a standard like JSON, leading to compatibility issues.

Example: A RESTless API Using Flask

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/getUserData", methods=["POST"])  # ❌ Wrong: Should use GET instead of POST
def get_user_data():
    """Returns mock user data"""
    return jsonify({"username": "JohnDoe", "email": "[email protected]"})

@app.route("/updateUser", methods=["POST"])  # ❌ Wrong: Should use PUT or PATCH
def update_user():
    """Mock update user function"""
    data = request.json
    return jsonify({"message": "User updated", "updated_data": data})

if __name__ == "__main__":
    app.run()

Problems in this Flask API:

  • ❌ Uses POST for retrieving data instead of GET.
  • ❌ Uses POST for updating data instead of PUT/PATCH.
  • ❌ Uses action-based URLs (/getUserData and /updateUser) instead of resource-based URLs.

What This Should Look Like Instead (Proper RESTful API in Flask):

@app.route("/users/<int:user_id>", methods=["GET", "PUT"])
def user(user_id):
    if request.method == "GET":
        return jsonify({"id": user_id, "username": "JohnDoe"})
    if request.method == "PUT":
        data = request.json
        return jsonify({"message": "User updated", "updated_data": data})

βœ… Now follows REST principles:

  • GET /users/123/ β†’ Fetch user
  • PUT /users/123/ β†’ Update user

Django REST (RESTful) API

1. Installing Dependencies

pip install django djangorestframework

2. Update setup.py in Root Directory

install_requires=[
    ...,
    "Django",
    "djangorestframework",
]

3. Navigate to src/example_python_project/api/

cd src/example_python_project/api/

4. Run the Django Project Create Command

django-admin startproject django_app .

5. Add rest_framework to django_app/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",  # Add Django REST Framework
]

6. Add Allowed Hosts

Find the allowed hosts entry and add '*'

ALLOWED_HOSTS = ['*']

7. Use SQLite for database (No Change Needed)

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

8. Run the start command

NOTE: Ensure you are still in the src/example_python_project/api/ directory

python manage.py startapp rest_api

This command will create:

src/example_python_project/api/rest_api/

9. Add Basic API

src/example_python_project/api/rest_api/views.py

from django.shortcuts import render
from django.http import JsonResponse

# Create your views here.
def health_check(request):
    """Health check endpoint for the Django API."""
    return JsonResponse({"status": "OK"})

src/example_python_project/api/rest_api/urls.py (May need to be created manually)

from django.urls import path
from .views import health_check

urlpatterns = [
    path("health/", health_check, name="health_check"),
]

src/example_python_project/api/django_app/urls.py

"""
URL configuration for django_app project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("example_python_project.api.rest_api.urls")),  # Include REST API routes
]

10. Run Migrations & Create Superuser

NOTE: Ensure you are still in the src/example_python_project/api/ directory

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser  # Follow the prompts

11. Run the server

python manage.py runserver
  • visit: http://127.0.0.1:8000/admin/
  • visit: http://127.0.0.1:8000/api/health/
⚠️ **GitHub.com Fallback** ⚠️