Creando una API REST en Python con Django

Vamos a construir una API RESTful usando Django sin bibliotecas o marcos externos. Nuestra API estará basada en JSON y realizará operaciones CRUD en un modelo ficticio de artículo de carrito.

Introducción

Django es un marco web Python potente que se utiliza para crear aplicaciones web seguras y escalables rápidamente con menos esfuerzo. Se hizo popular debido a su baja barrera de entrada y su fuerte comunidad que usa y desarrolla el marco.

En esta guía, vamos a construir una API RESTful usando Django sin bibliotecas externas. Cubriremos los conceptos básicos de Django e implementaremos una API basada en JSON para realizar operaciones CRUD para una aplicación de carrito de compras.

¿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.

Combinamos estos dos conceptos para construir una API REST(full), una API que se ajusta a las restricciones del estilo arquitectónico REST. Avancemos y hagamos uno, usando Python y Django.

Configuración de Django y nuestra aplicación

Como se mencionó anteriormente, Django es un marco web que promueve el rápido desarrollo de servicios web seguros y escalables.

Nota: Usaremos la versión 3.1 de Django, ya que es la última versión en el momento de escribir este artículo.

Antes de instalar Django, por si acaso y en nombre de aislar las dependencias, hagamos un entorno virtual:

1
$ python3 -m venv env

En algunos editores de código, lo encontrará ya activado. Si no, puede ir al directorio de scripts dentro del entorno y ejecutar activar.

En Windows:

1
$ env\scripts\activate

En Mac o Linux:

1
$ . env/bin/activate

Ahora, sigamos adelante e instalemos Django a través de pip:

1
$ pip install django

Una vez instalado, podemos crear nuestro proyecto. Si bien puede hacerlo manualmente, es mucho más conveniente comenzar con un proyecto de esqueleto a través de Django mismo.

La herramienta django-admin nos permite crear un proyecto esqueleto en blanco en el que podemos comenzar a trabajar de inmediato. Viene incluido con Django, por lo que no es necesaria ninguna instalación adicional.

Comencemos el proyecto invocando la herramienta, así como el comando startproject, seguido del nombre del proyecto:

1
$ django-admin startproject shopping_cart

Esto crea un proyecto esqueleto simple en el directorio de trabajo. Cada proyecto de Django puede contener varias aplicaciones; sin embargo, crearemos una. Invoquemos el archivo manage.py, creado a través del comando startproject para iniciar una aplicación:

1
2
$ cd shopping_cart
$ python manage.py startapp api_app

Una vez creada, la estructura de nuestro proyecto se verá algo así como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
> env
> shopping_cart
  > api_app
    > migrations
    __init__.py
    admin.py
    apps.py
    models.py
    tests.py
    views.py
  > shopping_cart
    __init__.py
    asgi.py
    settings.py
    urls.py
    wsgi.py
  manage.py

El shopping_cart de nivel superior es el directorio raíz de Django y comparte el nombre con el nombre del proyecto. El directorio raíz es el puente entre el marco y el proyecto en sí y contiene varias clases de configuración, como manage.py, que se utiliza para activar aplicaciones.

La api_app es la aplicación que estamos desarrollando, y puede haber muchas aquí. Cada uno tiene algunos scripts predeterminados que modificaremos para adaptarlos a la funcionalidad CRUD.

El carrito de la compra de bajo nivel es el directorio del proyecto, que contiene archivos relacionados con la configuración, como settings.py, que alojará todas las propiedades de nuestra aplicación.

Django es un Model-View-Controller (MVC). Es un patrón de diseño que separa una aplicación en tres componentes: el modelo que define los datos que se almacenan y con los que se interactúa, la vista que describe cómo se presentan los datos al usuario y el controlador que actúa como intermediario entre el modelo y la vista. Sin embargo, la interpretación de Django de este patrón es ligeramente diferente de la interpretación estándar. Por ejemplo, en un marco MVC estándar, la lógica que procesa las solicitudes HTTP para administrar los artículos del carrito de compras viviría en el controlador.

En Django, esa lógica reside en el archivo que contiene vistas. Puedes leer más sobre su interpretación [aquí](https://docs.djangoproject.com/en/3.1/faq/general/#django-appears-to-be-a-mvc-framework-but-you-call- el-controlador-la-vista-y-la-vista-la-plantilla-cómo-es-que-no-use-los-nombres-estándar). Comprender el concepto central de MVC, así como la interpretación de Django, hace que la estructura de esta aplicación sea más fácil de entender.

Cada proyecto de Django viene preinstalado con algunas aplicaciones (módulos) de Django. Estos se utilizan para autenticación, autorización, sesiones, etc. Para que Django sepa que también nos gustaría incluir nuestra propia aplicación, api_app, tendremos que incluirla en la lista INSTALLED_APPS.

Hagamos una lista yendo al archivo settings.py y modificando la lista para incluir nuestra propia aplicación:

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

Una vez en la lista, hemos terminado con la configuración del proyecto. Sin embargo, no hemos terminado con los cimientos sobre los que se construirá nuestra aplicación. Antes de desarrollar la funcionalidad CRUD, necesitaremos un modelo para trabajar como nuestra estructura de datos básica.

Nota: Iniciar un proyecto Django, de forma predeterminada, también preparará una base de datos SQLite para ese proyecto. No necesita configurarlo en absoluto: solo definir modelos y llamar a las funciones CRUD relevantes iniciará un proceso bajo el capó que hace todo por usted.

Definición de un modelo {#definición de un modelo}

Comencemos con un modelo simple y básico: el CartItem, que representa un artículo que aparece en un sitio web ficticio de comercio electrónico. Para definir modelos que Django puede recoger, modificamos el archivo api_app/models.py:

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()

Aquí, estamos aprovechando el módulo db integrado, que tiene un paquete de modelos dentro. La clase Modelo representa, bueno, un modelo. Tiene varios campos como CharField, IntegerField, etc. que se utilizan para definir el esquema del modelo en la base de datos. Estos son mapeados, bajo el capó, por el ORM de Django cuando desea guardar una instancia de un modelo en la base de datos.

Existen varios campos y están diseñados para funcionar bien con bases de datos relacionales, lo que también haremos aquí. Sin embargo, para bases de datos no relacionales, no funciona muy bien debido a una diferencia inherente en cómo se almacenan los datos.

Si desea trabajar con una base de datos no relacional, como MongoDB, consulte nuestra Guía para usar el motor Django MongoDB.

Para realizar cambios en los esquemas del modelo, como lo que acabamos de hacer, debemos llamar a Migraciones de Django. Las migraciones son bastante fáciles de hacer, sin embargo, tendrá que ejecutarlas cada vez que desee persistir un cambio en el esquema.

Para hacerlo, llamaremos al archivo manage.py y pasaremos los argumentos makemigrations y migrate:

1
2
$ python manage.py makemigrations  # Pack model changes into a file
$ python manage.py migrate  # Apply those changes to the database

La operación migrar debería dar como resultado algo como esto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Operations to perform:
  Apply all migrations: admin, api_app, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying api_app.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

Si sus migraciones no se ejecutaron correctamente, revise los pasos anteriores para asegurarse de que está configurado correctamente antes de continuar.

El sitio de administración de Django

Al crear aplicaciones, Django crea automáticamente un sitio de administración, destinado a ser utilizado por el desarrollador para probar cosas y darles acceso a los formularios generados para los modelos registrados. Solo está destinado a ser utilizado como un panel útil durante el desarrollo, no como el panel de administración real, que crearía por separado si desea tener uno.

El módulo admin de django.contrib es el paquete que nos permite personalizar el admin-sitio.

Para aprovechar la creación automática de formularios y la gestión de modelos de Django, tendremos que registrar nuestro modelo en admin.site.

Vayamos a api_app/admin.py y registremos nuestro modelo:

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

admin.site.register(CartItem)

Ahora, querremos crear un usuario que pueda acceder a este tablero y usarlo. Vamos a crear una cuenta de superadministrador y confirmar que todos estos cambios se realizaron con éxito:

1
$ python manage.py createsuperuser

Para crear una cuenta, deberá proporcionar un nombre de usuario, correo electrónico y contraseña. Puede dejar el correo electrónico en blanco. La contraseña no se refleja cuando escribe. Solo escribe y presiona enter:

1
2
3
4
5
Username (leave blank to use 'xxx'): naazneen
Email address:
Password:
Password (again):
Superuser created successfully.

Finalmente, ejecutemos nuestra aplicación para ver si las cosas funcionan según lo previsto:

1
$ python manage.py runserver

La aplicación se inicia en nuestro localhost (127.0.0.1) en el puerto 8000 de forma predeterminada. Naveguemos un navegador a http://127.0.0.1:8000/admin:

Admin page after successful Django setup

Ahora que nuestros modelos de aplicaciones y bases de datos están configurados, concentrémonos en desarrollar la API REST.

Crear una API REST en Django

La aplicación Django está lista: la configuración está definida, nuestra aplicación está preparada, el modelo está en su lugar y hemos creado un usuario administrador para verificar que el modelo está registrado en el panel de administración.

Ahora, implementemos la funcionalidad CRUD para nuestro modelo.

Creación de entidades: el controlador de solicitudes POST

Las solicitudes POST se utilizan para enviar datos al servidor. Por lo general, contienen datos en su cuerpo que se supone que deben almacenarse. Al completar formularios, cargar imágenes o enviar un mensaje, las solicitudes ‘POST’ se envían con esos datos, que luego se manejan y guardan en consecuencia.

Vamos a crear una vista de Django para aceptar datos del cliente, llenar una instancia de modelo con ella y agregarla a la base de datos. Esencialmente, podremos agregar un artículo a nuestro carrito de compras con nuestra API. Las vistas en Django se pueden escribir puramente como funciones o como métodos de una clase. Vamos a utilizar Vistas basadas en clases.

Para agregar una vista, modificaremos el archivo api_app_views.py y agregaremos un método post() que recibe una solicitud POST. Escribirá el cuerpo de la solicitud entrante en un diccionario y creará un objeto CartItem, manteniéndolo en la base de datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.views import View
from django.http import JsonResponse
import json
from .models import CartItem

class ShoppingCart(View):
    def post(self, request):

        data = json.loads(request.body.decode("utf-8"))
        p_name = data.get('product_name')
        p_price = data.get('product_price')
        p_quantity = data.get('product_quantity')

        product_data = {
            'product_name': p_name,
            'product_price': p_price,
            'product_quantity': p_quantity,
        }

        cart_item = CartItem.objects.create(**product_data)

        data = {
            "message": f"New item added to Cart with id: {cart_item.id}"
        }
        return JsonResponse(data, status=201)

Usando el módulo json, decodificamos y analizamos el cuerpo de la solicitud entrante en un objeto con el que podemos trabajar, y luego extrajimos esos datos en las variables p_name, p_price y p_quantity.

Finalmente, creamos un diccionario product_data para contener nuestros campos y sus valores, y conservamos un CartItem en nuestra base de datos, a través del método create() de la clase Model, llenándolo con nuestro datos_producto.

Tenga en cuenta el uso de la clase JsonResponse al final. Usamos esta clase para convertir nuestro diccionario de Python en un objeto JSON válido que se envía a través de HTTP al cliente. Establecemos el código de estado en 201 para indicar la creación de recursos en el extremo del servidor.

Si ejecutamos nuestra aplicación e intentamos llegar a este punto final, Django rechazaría la solicitud con un error de seguridad. De forma predeterminada, Django agrega una capa de protección para Ataques de falsificación de solicitudes entre sitios (CSRF). En la práctica, este token se almacena en las cookies de nuestro navegador y se envía con cada solicitud realizada al servidor. Como esta API se utilizará sin navegador ni cookies, las solicitudes nunca tendrán un token CSRF. Por lo tanto, debemos decirle a Django que este método POST no necesita un token CSRF.

Podemos lograr esto agregando un decorador al método dispatch de nuestra clase que establecerá csrf_exempt en True:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCart(View):

    def post(self, request):
        data = json.loads(request.body.decode("utf-8"))
        ...

Ahora, tenemos los modelos que almacenan nuestros datos y una vista que puede crear un nuevo artículo del carrito, a través de una solicitud HTTP. Lo único que queda por hacer es decirle a Django cómo tratar las URL y sus respectivos controladores. Para cada URL accedida, tendremos un mapeo de vista adecuado que lo maneje.

Se considera una buena práctica escribir las URL respectivas en cada aplicación y luego incluirlas en el archivo urls.py del proyecto, en lugar de tenerlas todas en el nivel superior desde el principio.

Comencemos modificando el urls.py del proyecto, en el directorio shopping_cart:

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('', include('api_app.urls')),
]

Asegúrese de importar la biblioteca include desde django.urls, no se importa de forma predeterminada.

El primer argumento es la ruta de la cadena y el segundo es de donde obtenemos las URL. Si nuestra ruta es '', o está vacía, significa que las URL de nuestra API serán la ruta raíz de la aplicación web.

Ahora necesitamos agregar los puntos finales para urls.py de nuestra aplicación API. En la carpeta api_app, crearemos un nuevo archivo llamado urls.py:

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

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

Similar al propio urls.py del proyecto, el primer argumento es la ruta secundaria donde nuestras vistas serían accesibles, y el segundo argumento son las vistas mismas.

Finalmente, podemos ejecutar la aplicación. Como antes, utilizaremos el archivo manage.py y pasaremos el argumento runserver:

1
$ python manage.py runserver

Abramos una terminal y enviemos una solicitud POST a nuestro punto final. Puede usar cualquier herramienta aquí, desde herramientas más avanzadas como Postman, hasta herramientas simples basadas en CLI como curl:

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

Si todo funciona bien, será recibido con un mensaje:

1
2
3
{
    "message": "New item added to Cart with id: 1"
}

Si visita http://127.0.0.1:8000/admin/api_app/cartitem/, también aparecerá en la lista el elemento del carrito que hemos agregado.

Recuperación de entidades: el controlador de solicitudes GET

Hagamos un controlador para las solicitudes GET, que normalmente envían los clientes cuando les gustaría recibir alguna información. Dado que tenemos un CartItem guardado en la base de datos, tiene sentido que alguien quiera recuperar información sobre él.

Asumiendo la posibilidad de más de un elemento, iteraremos sobre todas las entradas CartItem y agregaremos sus atributos en un diccionario, que se convierte fácilmente en una respuesta JSON para el cliente.

Modifiquemos la vista ShoppingCart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCart(View):

    def post(self, request):
        ...

    def get(self, request):
        items_count = CartItem.objects.count()
        items = CartItem.objects.all()

        items_data = []
        for item in items:
            items_data.append({
                'product_name': item.product_name,
                'product_price': item.product_price,
                'product_quantity': item.product_quantity,
            })

        data = {
            'items': items_data,
            'count': items_count,
        }

        return JsonResponse(data)

El método count() cuenta el número de ocurrencias en la base de datos, mientras que el método all() las recupera en una lista de entidades. Aquí, extraemos sus datos y los devolvemos como una respuesta JSON.

Enviemos una solicitud GET a nuestro punto final:

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

Esto da como resultado una respuesta JSON al cliente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "items": [
        {
            "product_name": "name",
            "product_price": 41.0,
            "product_quantity": 1
        },
    ],
    "count": 1
}

Actualización de entidades: el controlador de solicitudes PATCH

Podemos persistir y recuperar datos a través de nuestra API, sin embargo, es igualmente importante poder actualizar entidades ya persistentes. Las solicitudes PATCH y PUT entran en juego aquí.

Una solicitud PUT reemplaza por completo el recurso dado. Mientras que una solicitud PATCH modifica una parte del recurso dado.

En el caso de PUT, si el contexto de recurso dado no existe, creará uno. Para realizar una solicitud PATCH, el recurso ya debe existir. Para esta aplicación, solo queremos actualizar un recurso si ya existe uno, por lo que usaremos una solicitud PATCH.

Los métodos post() y get() están ubicados en la misma clase de vista ShoppingCart. Esto se debe a que afectan a más de un elemento del carrito de compras.

Una solicitud de PATCH solo afecta a un artículo del carrito. Así que crearemos una nueva clase que contendrá esta vista, así como el futuro método delete(). Agreguemos un controlador de solicitud PATCH a api_app/views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCartUpdate(View):

    def patch(self, request, item_id):
        data = json.loads(request.body.decode("utf-8"))
        item = CartItem.objects.get(id=item_id)
        item.product_quantity = data['product_quantity']
        item.save()

        data = {
            'message': f'Item {item_id} has been updated'
        }

        return JsonResponse(data)

Recuperaremos el elemento con una identificación dada y lo modificaremos antes de guardarlo nuevamente.

Dado que no queremos que el cliente pueda cambiar el precio o el nombre del producto, solo estamos haciendo que la cantidad del artículo en el carrito de compras sea de tamaño variable. Luego, llamamos al método save() para actualizar la entidad ya existente en la base de datos.

Ahora, también necesitamos registrar un punto final para esto, tal como lo hicimos para el punto final cart-items/:

api_app/urls.py:

1
2
3
4
5
6
7
8
from django.urls import path
from .views import ShoppingCart, ShoppingCartUpdate


urlpatterns = [
    path('cart-items/', ShoppingCart.as_view()),
    path('update-item/<int:item_id>', ShoppingCartUpdate.as_view()),
]

Esta vez, no solo confiamos en el verbo HTTP. La última vez, enviar una solicitud POST a /cart-items resultó en la llamada del método post(), mientras que enviar una solicitud GET resultó en la ejecución del método get().

Aquí, estamos agregando una URL-variable - /<int:item_id>. Este es un componente dinámico en la URL, que se asigna a la variable item_id de la vista. Según el valor proporcionado, el elemento adecuado se recupera de la base de datos.

Enviemos una solicitud PATCH a http:127.0.0.1:8000/update-item/1 con los datos apropiados:

1
$ curl -X PATCH http://127.0.0.1:8000/update-item/1 -d "{\"product_quantity\":\"3\"}"

Esto da como resultado una respuesta JSON:

1
2
3
{
    "message": "Item 1 has been updated"
}

También verifiquemos esto a través del Panel de administración en: http://127.0.0.1:8000/admin/api_app/cartitem/1/change/.

Eliminación de entidades: el controlador de solicitudes DELETE

Finalmente, la última pieza de la funcionalidad CRUD: eliminar entidades.

Para eliminar el artículo del carrito, usaremos la misma clase ShoppingCartUpdate ya que solo afecta a un artículo. Le agregaremos el método delete() que toma la ID del elemento que nos gustaría eliminar.

De manera similar a como save() el elemento cuando lo actualizamos con nuevos valores, podemos usar delete() para eliminarlo. Agreguemos el método delete() a api_app/views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...
@method_decorator(csrf_exempt, name='dispatch')
class ShoppingCartUpdate(View):

    def patch(self, request, item_id):
        ...

    def delete(self, request, item_id):
        item = CartItem.objects.get(id=item_id)
        item.delete()

        data = {
            'message': f'Item {item_id} has been deleted'
        }

        return JsonResponse(data)

Y ahora, enviemos la solicitud DELETE y proporcionemos el ID del elemento que nos gustaría eliminar:

1
curl -X "DELETE" http://127.0.0.1:8000/update-item/1

Y obtendremos la siguiente respuesta:

1
2
3
{
    "message": "Item 1 has been deleted"
}

Visitar http://127.0.0.1:8000/admin/api_app/cartitem/ verifica que el elemento ya no está allí.

Conclusión

En esta breve guía, hemos repasado cómo crear una API REST en Python con Django. Hemos repasado algunos de los fundamentos de Django, comenzamos un nuevo proyecto y una aplicación dentro de él, definimos los modelos necesarios e implementamos la funcionalidad CRUD.

El código completo de esta aplicación se puede encontrar aquí. .