Programación Back-End

Unidad 3: API RESTful con Integración de Base de Datos

The REST Framework

Redactado por Criz
Publicado el 22 de agosto de 2024 a las 13:26
Última actualización el 22 de agosto de 2024 a las 13:26

Acciones rapidas
Editar
Siguiente
Previo

The REST Framework

Django es de código abierto, esto permite a la comunidad generar librerías que apoyen la misión de Django. Una de estas librerías se llama rest_framework, y es la que aprenderemos a usar, pues es muy importante conocerla.

La cosa desde aquí se pone oscura. Si has llegado hasta aquí, date una palmada en la espalda, ya que posees lo básico-intermedio de Django.

Ahora, vamos a pasar por el horroroso, insípido, tortuoso, complejo mundo de las APIs.

Requisitos

Necesitaremos una forma de probar nuestro api, puede ser:

Yo usare la extension de Visual Studio Code REST Client.

Conceptos clave

Estos conceptos son importantes tenerlos en cuenta:

  1. serializer: Un serializador permite convertir un modelo Django en un formato orientado a texto. En resumen, permite la conversión de un queryset (modelo) a un formato como JSON, XML, etc.
  2. queryset: Es, en resumen, una consulta que se envía a un modelo. Siempre devuelve uno o más registros correspondientes al modelo seleccionado.
  3. validators: Este es más perteneciente a un modelo de Django, se refiere a las validaciones para que una entrada corresponda al formato de un modelo dado. (El tipo de dato, la longitud máxima, formato como email o numérico, etc.)
  4. APIView: No es nada más que una vista que controla una API respecto a un modelo. Por ejemplo, podemos crear una vista que solo sirva para listar (obtener) y crear nuevos registros dentro de nuestra base de datos respecto a un modelo.
  5. Class based views: Hemos usado vistas funcionales (function based views), pero ahora toca usar vistas basadas en clases Python. Que tienen un comportamiento similar, pero más enfocado a objetos. Lo cual viene de la perra, pues literal, nuestros modelos son clases Python que Django relaciona con registros.
  6. stateless: Dependiendo de una petición, la API devuelve un JSON o XML, etc., según corresponda. En los navegadores web, suele responder con un panel visual.

RESTful framework instalación

El sitio oficial del proyecto está en https://www.django-rest-framework.org/

Recuerda tener un entorno virtual de Python creado y cargado!

Como toda librería de Python, rest_framework debes instalarla desde pip:
pip install djangorestframework

Documentacion de instalacion disponible en https://www.django-rest-framework.org/#installation

Una vez instalado, debemos dirigirnos a nuestro archivo settings.py dentro de la carpeta del proyecto (ProyectoDjango/):

1
2
3
4
5
INSTALLED_APPS = [
    ...
    # 👇 debemos registrar la librería, ya que es una app de Django
    'rest_framework',
]

Y ya está, la instalación ha terminado, ahora vamos a usarla.

Una API simple

Montaremos nuestra API en una nueva aplicación, para ello simplemente usamos el comando:
py manage.py startapp api
En mi caso la llamare API, muy clarificador.
Como es de costumbre, tenemos que registrar la app en el archivo settings.py de nuestro proyecto.

Serializador

Un serializador permite transformar un queryset en un formato estructurado plano, como JSON, XML, etc...
Estos se definen en un archivo por separado que por convención es llamado serializers.py dentro de la aplicación api o también lo podemos crear en la aplicación tienda que es donde está el modelo.
En mi caso, lo creare dentro de api:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from rest_framework import serializers # para heredar nuestro serializador
from tienda.models import Producto # para indicar el modelo a serializar

# por convension se le llama 'ModeloSerializer'
class ProductoSerializador(serializers.ModelSerializer):
    class Meta: # al igual que con los formularios, podemos heredar un modelo
        model = Producto
        # indicamos que campos del modelo sera tomados en cuenta
        fields = ["slug", "nombre", "descripcion", "stock", "precio", "creacion"]
        #               👇 el slug no sera editable. (ya que nuestro modelo cuenta con un metodo para controlarlo)
        extra_kwargs = {"slug": {"read_only": True}}

Como puedes ver, es muy simple el serializador.

Vista basada en clase

Una Class Based View 🍷 es una vista basada en clases, que se comporta casi idéntico a una funcional, en REST framework debemos usar clases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from rest_framework import generics # para heredar clases a nuestras Class Based Views 🍷
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser # importamos los permisos que estaremos usando
from .serializers import ProductoSerializador # nuestro serializador
from tienda.models import Producto
...
#                               👇 esta vista se encarga de LISTAR (RETRIEVE en C'R'UD)
class ProductoListar(generics.ListAPIView):
    serializer_class = ProductoSerializador
    permission_classes = [AllowAny] # si es AllowAny, entonces esta linea no es necesaria
    # tambien esta permission_classes = [IsAuthenticated] # solo permite a usuarios con una sesion activa
    queryset = Producto.objects.all()

#                                   👇 esta nos permite CREAR (CREATE en 'C'RUD)
class ProductoCrear(generics.CreateAPIView):
    serializer_class = ProductoSerializador
    permission_classes = [IsAdminUser] # solo permite a usuarios con is_superuser o is_staff
    queryset = Producto.objects.all()

Declaramos la ruta

En nuestro archivo urls.py que debemos crear dentro de api, declaramos la ruta y su vista:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from django.urls import path
import api.views as app

app_name = "api"

urlpatterns = [
    #                           👇 debemos llamar este metodo en las Class Based Views 🍷
    path('productos/', app.ProductoListar.as_view(), name="listar"),
    #                👇 las rutas deben ser claras
    path('productos/crear/', app.ProductoCrear.as_view(), name="crear"),
]

Luego incluimos este urls.py dentro de urls.py de nuestro proyecto:

1
2
3
4
urlpatterns = [
    ...
    path('api/', include("api.urls", namespace="api")),
]

Ojalá tener una sesión iniciada, así podemos ver todas las rutas definidas. Simplemente vamos a Iniciar Sesión.

Ahora, si nos dirigimos a http://127.0.0.1:8000/api/productos/ deberíamos ver algo así:

Nótese que esta lista todos los productos, tal cual le hemos dicho.
Esta vista solo cumple esa función de listar.

Y si vamos a http://127.0.0.1:8000/api/productos/crear/:

En esta vista podemos observar que hay un formulario más abajo, que permite rellenar los campos, esto es así ya que heredamos la vista de generics.CreateAPIView, que permite crear productos.

En resumen, esto es muy simple. Definimos vistas y indicamos como deben comportarse.

Probando la API

Pero, las APIs no las usa un usuario a través de un navegador, estas son usadas por aplicaciones que las implementan. Estas pueden ser el Front-End del sitio u otra aplicación de terceros que simplemente las usan.

Ahora haremos una pequeña prueba con la extensión de VSC para probarla, tenemos que crear un archivo .http donde queramos.

Yo la creare dentro de la app api llamándola API-TESTER.http:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
### LISTAR
GET http://127.0.0.1:8000/api/productos/ HTTP/1.1
content-type: application/json

### CREAR
POST http://127.0.0.1:8000/api/productos/ HTTP/1.1
content-type: application/json

{
    "nombre": "Soldadura de madera",
    "descripcion": "Para roble y abedul",
    "stock": 4,
    "precio": 19000
}

Aparecerá un botón Send request, si le damos se ejecutará la consulta y nos mostrará la respuesta en una ventana dividida.

En el caso del POST, le hemos dicho a nuestra vista que solo peticiones provenientes de sesiones con un usuario con permiso de administrador serán aceptadas, por lo que si lo ejecutamos tal cual nos lanzara un error mencionando que no se encontraron las credenciales.

1
2
3
{
  "detail": "Las credenciales de autenticación no se proveyeron."
}

Esto lo podemos solucionar agregando el header llamado Authorization, que es como las APIs suelen autentificar a un usuario, por lo que, en nuestro archivo .http el método POST quedaría:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
### CREAR
POST http://127.0.0.1:8000/api/productos/crear/ HTTP/1.1
Authorization: Basic Criz:12345
content-type: application/json
{
    "nombre": "Soldadura de madera",
    "descripcion": "Para roble y abedul",
    "stock": 4,
    "precio": 19000
}

Y así podremos ejecutar la petición, la cual debería crear el producto "Soldadura de madera" y guardarlo en la base de datos.

Nótese que el campo slug ha sido rellenado automáticamente, igualmente con el campo creacion. Esto no es por la API, es como está construido el modelo.
El slug por otra parte debemos especificarle a la API que no es editable. Ya que nuestro modelo lo crea por nosotros automáticamente.
También cabe mencionar que nuestra respuesta fue un JSON, y en nuestro navegador web fue un panel. Esto se debe al stateless. Responde como debe según el contexto de la petición.

El UPDATE y DELETE

Ahora bien, quedan hacer la forma de actualizar y eliminar un registro, esto se realiza con el método PUT para editar y DELETE para eliminar.

Entonces, dentro de views.py de nuestra aplicación api ponemos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class ProductoActualizar(generics.UpdateAPIView):
    serializer_class = ProductoSerializador
    permission_classes = [IsAdminUser]# solo usuarios con sesion activa y con permisos de administrador (is_staff o is_superuser)
    queryset = Producto.objects.all()
    lookup_field = "slug" # el SLUG del modelo es el que debe tener en cuenta para buscar el producto
    lookup_url_kwarg = "uid" # el UID es como se lo pasamos en la url

class ProductoEliminar(generics.DestroyAPIView):
    serializer_class = ProductoSerializador
    permission_classes = [IsAdminUser]
    queryset = Producto.objects.all()
    lookup_field = "slug"
    lookup_url_kwarg = "uid"

Y luego registramos las rutas en api/urls.py:

1
2
3
4
5
6
7
8
...
urlpatterns = [
    ...
    #                           👇 slug es tipo de dato, no confundir el el campo de nuestro modelo
    path('productos/actualizar/<slug:uid>/', app.ProductoActualizar.as_view(), name="actualizar"),
    #                               👇 el estandar es llamarle pk, pero somos rebeldes
    path('productos/eliminar/<slug:uid>/', app.ProductoEliminar.as_view(), name="eliminar"),
]

Y ya está, ahora probamos con la extensión:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
### ACTUALIZAR
PUT http://127.0.0.1:8000/api/productos/actualizar/soldadura-de-madera// HTTP/1.1
content-type: application/json

{
    "nombre": "Isotopo para madera",
    "descripcion": "Para todas las maderas",
    "stock": 50,
    "precio": 10000
}

### ELIMINAR
DELETE http://127.0.0.1:8000/api/productos/eliminar/soldadura-de-madera/ HTTP/1.1
content-type: application/json

Obviamente, debemos especificar el header Authorization para poder ejecutarlas.

Nótese que cada una de las peticiones devuelven distintos códigos HTTP:

  • HTTP/1.1 200 OK: Para la petición LISTAR y ACTUALIZAR
  • HTTP/1.1 201 Created: Para la petición CREAR
  • HTTP/1.1 204 No Content: Para la petición ELIMINAR

Estos códigos son importantes a la hora de desarrollar un cliente que use nuestra API.

Estos códigos son llamados HTTP Response Status Codes y te recomendamos echarles un vistazo.

El header de Autorización

Para realizar las operaciones que están bajo IsAuthenticated, IsAdminUser, u otro permiso que requiera una sesión activa. Es necesario pasar un header que le permita a Django saber quiénes somos.
Para ello, en la petición HTTP, debajo debemos poner el header Authorization e indicar nuestras credenciales en nuestro API-TESTER.http tal que:

1
2
3
4
5
### CREAR
POST http://127.0.0.1:8000/api/productos/ HTTP/1.1
Authorization: Basic Criz:12345
content-type: application/json
...

El header Authorization esta estandarizado.
El formato es username:password dentro de Authorization.

Lo mismo para los otros métodos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
### ACTUALIZAR
PUT http://127.0.0.1:8000/api/productos/actualizar/soldadura-de-madera/ HTTP/1.1
Authorization: Basic Criz:12345
content-type: application/json

{
    "nombre": "Soldadura de madera",
    "descripcion": "Para todas las maderas",
    "stock": 5,
    "precio": 19000
}
1
2
3
4
### ELIMINAR
DELETE http://127.0.0.1:8000/api/productos/eliminar/soldadura-de-madera/ HTTP/1.1
Authorization: Basic Criz:12345
content-type: application/json

El obvio problema aquí es que el usuario y su contraseña están en texto plano, para evitar esto es que existe el sistema de autenticación JWT (que es solo un pez más dentro de la gran pecera)

Divide y conquistaras

Dejaremos lo del JWT y demás para el próximo capitulo, ya que como dije, la cosa se pone oscura.


Siguiente
Previo