Crear una API REST con Django REST Framework

En esta guía, crearemos una API REST en Python con Django, usando Django REST Framework para crear una aplicación de carrito de compras.

Introducción

Las API REST son una forma estándar de la industria para que los servicios web envíen y reciban datos. Utilizan métodos de solicitud HTTP para facilitar el ciclo de solicitud-respuesta y, por lo general, transfieren datos mediante JSON y, más raramente, HTML, XML y otros formatos.

En esta guía, crearemos una API REST en Python con Django, usando el Marco REST de Django para crear una aplicación de carrito de compras.

{.icon aria-hidden=“true”}

Nota: El código completo de esta aplicación se puede encontrar en GitHub.

¿Qué es una API REST?

REST (Transferencia de estado representacional) es una arquitectura estándar para construir y comunicarse con servicios web. Por lo general, exige que los recursos en la web se representen en un formato de texto (como JSON, HTML o XML) y se puede acceder a ellos o modificarlos mediante un conjunto predeterminado de operaciones. Dado que normalmente construimos API REST para aprovechar HTTP en lugar de otros protocolos, estas operaciones corresponden a métodos HTTP como GET, POST o PUT.

Una API (interfaz de programación de aplicaciones), como su nombre indica, es una interfaz que define la interacción entre diferentes componentes de software. Las API web definen qué solicitudes se pueden realizar a un componente (por ejemplo, un punto final para obtener una lista de elementos del carrito de compras), cómo realizarlas (por ejemplo, una solicitud GET) y sus respuestas esperadas.

En esta guía, combinaremos estos dos conceptos para construir una API REST(full), una API que se ajuste a las restricciones del estilo arquitectónico REST, utilizando Django REST Framework.

¿Qué es el marco REST de Django?

Django REST Framework (DRF) es un paquete creado sobre Django para crear API web. Una de las características más notables de Django es su Object Relational Mapper (ORM) que facilita la interacción con la base de datos de forma Pythonic.

Sin embargo, no podemos enviar objetos de Python a través de una red y, por lo tanto, necesitamos un mecanismo para traducir los modelos de Django en otros formatos como JSON, XML y viceversa. Este proceso a veces desafiante, también llamado serialización, se hace muy fácil con Django REST Framework.

{.icon aria-hidden=“true”}

Nota: Vale la pena señalar la diferencia entre crear una API REST con Django y con Django REST.

Puede crear aplicaciones web clásicas a través de Django y exponer su funcionalidad al mundo a través de las API REST. De hecho, ¡esto es bastante fácil de hacer! Sin embargo, Django REST Framework es más especializado para esta tarea, está construido sobre Django simple y facilita el proceso.

If you'd like to read on how to create REST APIs with the core Django framework - read out Guía para crear API REST en Python con Django.

Configuración de Django y nuestra aplicación

Django está diseñado para proyectos de Desarrollo rápido de aplicaciones (RAD). Vamos a configurar rápidamente un proyecto Django:

Comencemos por inicializar un entorno virtual, en aras de organizar las dependencias y sus efectos en otras dependencias, y activarlo:

1
2
3
4
5
$ mkdir drf_tutorial
$ cd drf_tutorial
$ python3 -m venv env
$ env\scripts\activate # Windows 
$ . env/bin/activate # MAC or Linux 

For more on virtualenv, read our Explicación del entorno virtual de Python!

Or, if you'd like to read about virtualenv alternatives, read our Guía para administrar entornos de Python con direnv y pyenv!

Luego, podemos instalar Django y Django REST Framework, dentro de ese entorno:

1
2
$ pip install django
$ pip install djangorestframework

Finalmente, podemos crear un proyecto y una aplicación, llamados api_app:

1
2
3
$ django-admin startproject shopping_cart
$ cd shopping_cart # Project contains app
$ python3 manage.py startapp api_app

Una vez que se crea la aplicación, debe registrarse en el archivo settings.py. Presentémoslo junto con algunas de las aplicaciones integradas, como admin y auth, que facilitan la funcionalidad de administración y el soporte de autenticación simple.

Abra el archivo en shopping_cart\settings.py y agregue el directorio api_app que hemos creado en la lista INSTALLED_APPS. Además, agreguemos rest_framework en la lista para que Django sepa que usaremos Django REST Framework (DRF de ahora en adelante):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api_app',
]

Una vez registrados, podemos aplicar la migración (inicializar la base de datos) y crear un superusuario para vigilar la base de datos:

1
2
$ python3 manage.py migrate  # Initialize database
$ python3 manage.py createsuperuser # Prompts for username and password

Con un superusuario instalado y una aplicación registrada, ¡podemos iniciar el servidor para aceptar solicitudes! Esto se hace fácilmente a través del comando runserver, desde manage.py:

1
$ python3 manage.py runserver

El proceso de generación, instalación y configuración de una aplicación web de Django, así como los componentes básicos de las aplicaciones web de Django (definición de modelos, registro de modelos, la interfaz de administración de Django, etc.) se cubren con más detalle en nuestra Guía de API REST de Core Django.

Creando una API REST en Django usando DRF

La aplicación Django está lista y podemos comenzar a desarrollar el modelo de dominio, la persistencia y la lógica empresarial.

Modelo de dominio

Vamos a crear un modelo simple, CartItem, para indicar un elemento del carrito de compras en línea, o mejor dicho, un producto. En el archivo api_app/models.py, definiremos nuestro modelo:

1
2
3
4
5
6
from django.db import models

class CartItem(models.Model):
    product_name = models.CharField(max_length=200)
    product_price = models.FloatField()
    product_quantity = models.PositiveIntegerField()

Una vez definido, registraremos nuestro modelo con Django, para que podamos acceder a él desde el panel de administración. Vaya a api_app/admin.py y agregue las siguientes líneas:

1
2
3
4
from django.contrib import admin
from .models import CartItem

admin.site.register(CartItem)

Una vez que se ha definido un nuevo modelo, necesitaremos hacer migraciones para que nuestro modelo se refleje en la base de datos. Desde el símbolo del sistema, ejecute lo siguiente:

1
2
$ python3 manage.py makemigrations
$ python3 manage.py migrate

¡El modelo está listo para ser utilizado! Las aplicaciones web transfieren con frecuencia datos de modelos de un extremo a otro. Naturalmente, es hora de implementar la característica más útil de DRF, ¡los serializadores!

Serializadores definen la representación de nuestro modelo en formato JSON y convierten instancias de objetos a un formato más transferible. Esto simplificará el análisis de datos para nuestra API. Los deserializadores hacen lo contrario: convierten datos JSON en nuestros modelos como instancias de objetos.

Usaremos un serializador para convertir nuestro objeto modelo a JSON antes de enviar la respuesta. Y cuando recibimos una solicitud JSON, nuestro serializador la convertirá al objeto modelo, CartItem en este caso.

Vamos a crear un archivo serializers.py en la carpeta api_app y escribir un ModelSerializer para nuestro modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from rest_framework import serializers
from .models import CartItem

class CartItemSerializer(serializers.ModelSerializer):
    product_name = serializers.CharField(max_length=200)
    product_price = serializers.FloatField()
    product_quantity = serializers.IntegerField(required=False, default=1)

    class Meta:
        model = CartItem
        fields = ('__all__')

En el archivo models.py, hemos establecido el atributo product_quantity de nuestro modelo como un campo obligatorio. Esto asegurará que siempre esté presente al guardar un objeto.

Sin embargo, cuando el usuario no ha especificado product_quantity, una suposición predeterminada razonable es que desea comprar un solo artículo. La API no debería arrojar un error en este caso y establecer product_quantity en 1 de forma predeterminada.

El serializador manejará este escenario con gracia, sin que tengas que escribir ninguna lógica de este tipo en views.py. Simplemente puede agregar validación y otras restricciones que sean necesarias para el atributo de la clase de serializador.

De forma predeterminada, requerido para cada campo se establece en Verdadero. Por lo tanto, el serializador no procederá a menos que los obtenga.

La clase APIView

Al igual que con Django puro, DRF permite tanto vistas basadas en clases como vistas basadas en funciones para la API.

En esta guía, favoreceremos las vistas basadas en clases.

Usaremos la clase APIView para representar vistas, que es una subclase de la clase View de Django. De esta manera obtenemos los métodos post(), get(), patch() y delete() que podemos usar para realizar sin esfuerzo operaciones CRUD en nuestro modelo CartItem, sin tener que manipularlo la capa de persistencia en absoluto!

{.icon aria-hidden=“true”}

Nota: Si bien es atractivo delegar toda la lógica subyacente a un marco, vale la pena señalar que probablemente trabajará con esta capa manualmente en una fecha posterior y comprenderá adecuadamente las bases de datos. es muy alentado.

Los métodos get(), post(), patch() y delete() se pueden usar junto con los métodos del modelo, como all(), save() y delete() para facilitar la funcionalidad CRUD para una aplicación.

Nuestra clase CartItemViews, que representa la vista, extenderá APIView:

1
2
class CartItemViews(APIView):
...

Creación de entidades: el controlador de solicitudes POST

Una solicitud POST se utiliza para enviar datos al servidor incluido en el cuerpo de la solicitud. Está destinado a ser utilizado cuando desee crear nuevas entidades. Vayamos a nuestras vistas y creemos un controlador de solicitudes POST para nuestro modelo CartItem.

Vayamos a api_app/views.py, cree una nueva clase con un método post() que recibirá un cuerpo de solicitud POST, valídelo y cree un objeto de clase CartItem en nuestra base de datos :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import CartItemSerializer
from .models import CartItem

class CartItemViews(APIView):
    def post(self, request):
        serializer = CartItemSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
        else:
            return Response({"status": "error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

Aquí puede observar que primero, creamos un objeto serializador a partir de request.data usando el CartItemSerializer que hemos creado previamente. La función is_valid() devuelve un valor booleano que indica si el cuerpo de la solicitud se puede usar para crear un objeto CartItem. Y el método save() creará una nueva instancia de CartItem.

La Respuesta debe inicializarse con los datos a devolver. Estos datos pueden ser una instancia de cualquier tipo de objeto de Python como bool, str, dict, etc.

Otros parámetros opcionales incluyen status que establece el código de respuesta HTTP, content-type que indica cómo se representarán los data, template_name que se puede usar si se selecciona HTMLRenderer y headers si queremos enviar explícitamente ciertos encabezados en la respuesta HTTP.

Configuremos y expongamos un punto final para usar nuestro método post(). Hacemos esto editando shopping_cart/urls.py e incluyendo los puntos finales expuestos de nuestra aplicación:

1
2
3
4
5
6
7
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api_app.urls')),
]

Con el punto final expuesto, querremos registrar la clase real CartItemViews como una vista para el usuario. Tenga en cuenta que esto no incluye una vista en el sentido de una GUI: es el controlador de solicitudes.

Hemos incluido api_app.urls aquí y delegamos la lógica que conecta la vista con el script urls.py dentro de api_app. En la carpeta api_app, cree un nuevo archivo llamado urls.py y vincule el localizador cart-items/ con la clase CartItemViews:

1
2
3
4
5
6
from django.urls import path
from .views import CartItemViews

urlpatterns = [
    path('cart-items/', CartItemViews.as_view())
]

El primer argumento de path() es la subruta donde nuestras vistas serían accesibles, y el segundo argumento es el nombre de la clase que creamos en views.py para procesar nuestra solicitud.

Ejecutando el servidor

Ejecutemos la aplicación y usemos nuestro punto final /api/cart-items/:

1
$ python3 manage.py runserver

Esto iniciará el servidor local en http://127.0.0.1:8000/.

En otra terminal, enviemos una solicitud POST a nuestro punto final con algunos datos:

1
$ curl -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/cart-items/ -d "{\"product_name\":\"name\",\"product_price\":\"41\",\"product_quantity\":\"1\"}"

La vista procesa la solicitud entrante y responde con los datos del producto, así como un estado:

1
2
3
4
5
6
7
8
9
{
    "status": "success",
    "data": {
        "id": 21,
        "product_name": "name",
        "product_price": 41.0,
        "product_quantity": 1
    }
}

El serializador acepta los datos JSON, los deserializa en un objeto concreto y luego los serializa nuevamente para devolver una respuesta.

Puede visitar http://127.0.0.1:8000/admin/api_app/cartitem/ y encontrará el elemento que acabamos de agregar.

También debe visitar http://127.0.0.1:8000/api/cart-items/ y verá otra característica notable de DRF, una API navegable.

Tenga en cuenta que no creamos ninguna página HTML asociada con la vista, pero DRF generó automáticamente una para nosotros:

DRF API web navegable después de crear el punto final \'api/cart-items\'

{.icon aria-hidden=“true”}

Nota: Si recibe un mensaje de error titulado "Plantilla no encontrada", asegúrese de haber incluido rest_framework en la matriz INSTALLED_APPS de shopping_cart/settings.py.

Dice que el método GET no está permitido porque aún no hemos creado un controlador GET para nuestro CartItemView. Sin embargo, hay un campo de entrada que le permitirá enviar una solicitud POST al punto final.

Solicitud de validación de datos

¿Qué sucede si alguien ingresa ingresa datos no válidos? Por ejemplo, una cadena para el atributo product_quantity, que obviamente no coincide con el tipo de datos esperado.

Intentemos hacer una solicitud no válida al punto final api/cart-items:

1
$ curl -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/cart-items/ -d "{\"product_name\":\"name\",\"product_price\":\"41\",\"product_quantity\":\"One\"}"

Esto daría como resultado una respuesta:

1
2
3
4
5
6
7
8
{
    "status": "error",
    "data": {
        "product_quantity": [
            "A valid integer is required."
        ]
    }
}

El error se presenta maravillosamente usando serializer.errors - y se nos pide que ingresemos un valor válido para el atributo product_quantity. El modelo sabe lo que espera y hemos proporcionado el tipo incorrecto.

Esta es una característica sorprendente del DRF: validación automática de datos. Especialmente para el arranque o la creación de prototipos, esto le ahorra el molesto proceso de validación de entradas simples. Sin embargo, también puede definir reglas de validación personalizadas, a través de validadores personalizados.

Si desea leer más sobre validadores, lea nuestra Guía de validadores de Django (¡próximamente!)

Recuperación de entidades: el controlador de solicitudes GET

Ahora que hemos agregado con éxito un artículo al carrito, definamos la lógica para recuperar esa entidad, junto con cualquier otra entidad que pueda estar en un carrito.

Hay dos formas típicas de recuperar recursos:

  • Podemos hacer una solicitud GET para enumerar todas las entidades vinculadas a un carrito.
  • Podemos recuperar una entidad en particular de nuestro carrito pasando su id como parámetro de URL.

Podemos obtener un objeto particular del modelo y serializar sus datos usando CartItemSerializer. Del mismo modo, también podemos obtener todos los objetos de nuestro modelo y serializar sus datos.

El último enfoque requiere que se pase también un argumento adicional, muchos:

1
serializer = CartItemSerializer(items, many=True)

Vamos a OBTENER un objeto, dado su id, y todos los demás artículos en ese carrito si no se ha proporcionado el id, modificando el archivo api_app/views.py :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
...
class CartItemViews(APIView):
    ...

    def get(self, request, id=None):
        if id:
            item = CartItem.objects.get(id=id)
            serializer = CartItemSerializer(item)
            return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)

        items = CartItem.objects.all()
        serializer = CartItemSerializer(items, many=True)
        return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)

Si se omite el argumento ‘id’ opcional, la solicitud devuelve todos los artículos del carrito en lugar de uno en particular y, en ambos casos, una ‘Respuesta’ le permite al cliente saber cómo le fue a la solicitud y se inyectan los datos serializados.

Vayamos a nuestro punto final api/cart-items/ con una solicitud GET:

1
$ curl -X GET http://127.0.0.1:8000/api/cart-items/

Esto obtendrá los resultados como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "status": "success",
    "data": [
        {
            "id": 1,
            "product_name": "name",
            "product_price": 41.0,
            "product_quantity": 1
        }
    ]
}

Como puede ver, CartItemSerializer(items, many=True) ha devuelto datos serializados en formato JSON: una lista de objetos. Alternativamente, podemos proporcionar el argumento id a través de la URL, como api/cart-items/1/. Una vez que registramos un punto final con una URL variable como esta, DRF vinculará automáticamente las variables de ruta a los argumentos de nuestra solicitud.

Ahora modifiquemos el urls.py de la aplicación y agreguemos la ruta - cart-items/<int:id>, que también apunta a nuestra clase CartItemViews.

En este punto, api_app/urls.py se vería así:

1
2
3
4
5
6
7
from django.urls import path
from .views import CartItemViews

urlpatterns = [
    path('cart-items', CartItemViews.as_view()),
    path('cart-items/<int:id>', CartItemViews.as_view())
]

Ahora, cuando llegamos al punto final api/cart-items/1, la variable 1 se resuelve en el argumento id del método get():

1
$ curl -X GET http://127.0.0.1:8000/api/cart-items/1

Esto daría como resultado la siguiente respuesta:

1
2
3
4
5
6
7
8
9
{
    "status": "success",
    "data": {
        "id": 1,
        "product_name": "name",
        "product_price": 41.0,
        "product_quantity": 1
    }
}

Aquí puede observar que CartItemSerializer(item) ha devuelto los datos de la instancia de CartItem como un solo objeto JSON en lugar de una matriz, ya que solo se espera que se devuelva un recurso.

Actualización de entidades: el controlador de solicitudes PATCH

Ahora podemos agregar y recuperar artículos del carrito y así alterar y observar directamente el estado del carrito. Ahora, necesitamos un punto final para actualizar el artículo que ya está en un carrito, como aumentar la cantidad, porque ¿quién no quiere más cosas?

Para actualizar objetos, podemos usar solicitudes POST, apuntando a un id determinado. Luego, podemos recuperar ese objeto, actualizarlo y guardarlo con el mismo id, manteniendo el cambio.

Sin embargo, normalmente no usará solicitudes POST para esto, aunque puede hacerlo. Para desacoplar la lógica de creación y actualización, usamos solicitudes PATCH para, bueno, parchar recursos existentes y cambiarlos.

La clase APIView nos proporciona la función patch(), que maneja las solicitudes de PATCH y actualiza los datos.

Volviendo de nuevo a api_app/views.py para agregar el controlador de solicitud PATCH como se muestra a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
class CartItemViews(APIView):
    ...    
    def patch(self, request, id=None):
        item = CartItem.objects.get(id=id)
        serializer = CartItemSerializer(item, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response({"status": "success", "data": serializer.data})
        else:
            return Response({"status": "error", "data": serializer.errors})

Presta mucha atención a esta línea:

1
serializer = CartItemSerializer(item, data=request.data, partial=True)

Aquí estamos pasando tres argumentos a nuestro serializador.

  • La instancia del modelo CartItem que queremos actualizar.
  • Los datos recibidos de la solicitud.
  • parcial=True para indicar que este puede no contener todos los campos de nuestro modelo CartItem.

Como necesitamos pasar una instancia real, tendremos que usar la función get() para recuperar primero un recurso y luego actualizarlo.

{.icon aria-hidden=“true”}

Nota: Al recuperar un recurso para actualizarlo, es mejor realizar una lógica de validación para asegurarse de que el recurso existe en primer lugar.

Y como estamos haciendo una actualización, validaremos nuestro serializador y luego lo guardaremos. Es hora de enviar una solicitud PATCH en api/cart-items/1 y actualizar el elemento:

1
$ curl -X PATCH http://127.0.0.1:8000/api/cart-items/1 -H 'Content-Type: application/json' -d '{"product_quantity":6}'

Esto resulta en:

1
2
3
4
5
6
7
8
9
{
    "status": "success",
    "data": {
        "id": 1,
        "product_name": "name",
        "product_price": 41.0,
        "product_quantity": 6
    }
}

La respuesta mostró una cantidad actualizada. También puede visitar http://127.0.0.1:8000/admin/api_app/cartitem/1/change/ y encontrará que se actualizó correctamente.

Eliminación de entidades: el controlador de solicitudes DELETE

Por lo general, agregamos, agregamos, agregamos y luego recordamos que queremos comprar algunos artículos en múltiplos, como regalos, hasta que llega la sección de facturación. La verificación de la realidad generalmente nos hace eliminar algunas cosas del carrito que realmente no necesitamos y reevaluar nuestra lógica.

Un usuario debe poder eliminar ciertos artículos de un carrito, ya sea que los agregue por accidente o simplemente cambie de opinión.

Para eliminar un artículo del carrito, implementemos la función delete(), pasando el id del objeto que nos gustaría eliminar. Luego, llamando a delete() en el propio modelo, podemos eliminarlo de la persistencia.

No necesitaremos usar un serializador para este propósito ya que no hay conversión entre datos y objetos concretos. En lugar de hacer CartItem.objects.get(), podemos usar la función get_object_or_404() que devolverá automáticamente una respuesta 404 cuando el objeto con el id dado no esté presente, ya que no lo haremos devolverá cualquier información sobre la entidad eliminada en sí.

De esta manera, sabemos si una entidad existe (no devolvió 404) o no, sin tener que serializarla y devolverla como respuesta.

Volvamos a api_app/views.py y agreguemos el método delete():

1
2
3
4
5
6
7
8
9
...
from django.shortcuts import get_object_or_404

class CartItemViews(APIView):
    ...
    def delete(self, request, id=None):
        item = get_object_or_404(CartItem, id=id)
        item.delete()
        return Response({"status": "success", "data": "Item Deleted"})

¡No te pierdas la nueva declaración de importación! Después de obtener el objeto, llamar a su método delete() lo elimina de la base de datos.

Intentemos eliminar el artículo de nuestro carrito:

1
$ curl -X "DELETE" http://127.0.0.1:8000/api/cart-items/1

Si el elemento está presente, la función debe devolver la siguiente respuesta:

1
2
3
4
{
    "status": "success",
    "data": "Item Deleted"
}

Cuando el elemento no está presente, la respuesta se vería así:

1
2
3
{
    "detail": "Not found."
}

Puede visitar http://127.0.0.1:8000/admin/api_app/cartitem/ y el artículo ya no está presente allí. ¡También puede visitar http://127.0.0.1:8000/api/cart-items/ para acceder a la API web navegable que acaba de crear con todas las operaciones CRUD!

Conclusión

Este tutorial mostró cómo podemos construir una API RESTful en Django usando Django REST Framework. Creamos un proyecto Django y le agregamos una aplicación api_app. Luego creamos un modelo CartItem y CartItemSerializer para manejar la serialización y deserialización de nuestro modelo.

Agregamos una vista basada en clases CartItemView para realizar operaciones CRUD en nuestro modelo. Agregamos un artículo al carrito usando post(), recuperamos todos los artículos y un artículo en particular usando get(). También creamos patch() para actualizar nuestros artículos y delete() para eliminar un artículo del carrito.

El código completo de esta aplicación se puede encontrar en GitHub.