Parte 3: Creación de modelos de Pydantic y nuestro primer usuario con FastAPI
¡Muy buenas! Ya vamos por el tercer tutorial de esta serie, como pasa el tiempo 🙀. En esta tercera parte vamos a ver cómo realizar el enrutamiento desde un archivo diferente al main, añadiremos los modelos de Pydantic y crearemos nuestro primer usuario con FastAPI.
Si queréis acceder al resto de los tutoriales, aquí tenéis los enlaces 👇
- Parte 1: Cómo crear una API REST COMPLETA con FastAPI, instalación y estructura
- Parte 2: Conexiones a bases de datos y creación de modelos con FastAPI
- Parte 4: Autenticación con JWT en FastAPI
- Parte 5: Cómo crear un CRUD con FastAPI
- Parte 6: Tests en FastAPI
Crear modelos de Pydantic
Como comentamos en el tutorial anterior, Pydantic es una librería de Python que se utiliza para la validación de datos. Como FastAPI dice en su documentación, este está basado en Pydantic así que será vital conocer su funcionamiento.
Por eso, antes de seguir voy a comentar en que casos nos será útil usar Pydantic:
- Definir requerimientos en la petición cuando nos envíen parámetros ya sea vía get, post, cabeceras, etc.
- Convertir los datos recibidos en el tipo requerido. Por ejemplo si enviamos vía get un parámetro llamado is_done que será igual a true (ejemplo: http://localhost:8000/todo?is_done=true), nosotros lo recibiremos como string y Pydantic se encargará de convertirlo a booleano.
- Validación de datos. Por ejemplo si necesitamos recibir un parámetro de tipo int validar que sea un número y si no lo es devolver un error en la respuesta.
- Documentación. Una de las mejores cosas de FastAPI es que genera una página de documentación de forma automática. Desde los modelos de Pydantic (y en otras partes también) podemos definir información adicional como vamos a ver en este tutorial.
Esto no puede quedar muy claro ahora, pero no os preocupéis porque vamos a verlo en acción en los siguientes pasos.
Dicho esto vamos a crear nuestro primer modelo de Pydantic. Para diferenciarlos de los modelos de peewee, estos los vamos a guardar en la carpeta /app/v1/schema y el único modelo que necesitamos de momento es de usuarios así que nos dirigimos a esa carpeta y creamos un archivo que se llamará user_schema.py y que contendrá el siguiente código:
from pydantic import BaseModel
from pydantic import Field
from pydantic import EmailStr
class UserBase(BaseModel):
email: EmailStr = Field(
...,
example="myemail@cosasdedevs.com"
)
username: str = Field(
...,
min_length=3,
max_length=50,
example="MyTypicalUsername"
)
class User(UserBase):
id: int = Field(
...,
example="5"
)
class UserRegister(UserBase):
password: str = Field(
...,
min_length=8,
max_length=64,
example="strongpass"
)
Como podéis observar, primero importamos BaseModel de Pydantic (bueno los tres imports son de Pydantic). Como explicamos anteriormente, todas las variables que definamos dentro de la clase que extienda de BaseModel, pasará por un proceso de validación y si hay algún error lanzará una excepción.
Después importamos la función Field. Esta función nos permite validar distintos tipos de datos, marcar si es obligatorio o no, tamaños máximos y mínimos, etc.
Por último importamos EmailStr. Esta clase la utilizaremos para tipar una variable como tipo email y validará si el email recibido es válido o no. Actualmente, hay que hacer una instalación adicional para usar esta clase así que lo haremos con el siguiente comando:
pip install "pydantic[email]"
Posteriormente, tenemos tres modelos, el primero UserBase extenderá de BaseModel y será compartido por los otros dos y luego tenemos User que aparte de los parámetros base tendrá el id. Este modelo lo emplearemos como respuesta cuando necesitemos retornar la información de un usuario.
Por último tenemos UserRegister que lo emplearemos como modelo cuando un usuario se quiera registrar.
Ahora vamos a explicar la clase UserBase en profundidad:
Primero definimos la variable que será de tipo EmailStr y será igual a Field. Como primer parámetro enviamos "..." que significa que ese campo será obligatorio, como segundo parámetro recibe example que es un dato informativo para el usuario y que podremos ver en la documentación más adelante.
La segunda variable es username. Esta será de tipo string y aquí recibe dos parámetros nuevos que son min_length y max_length. Esto significa que el string necesitará tener al menos 3 caracteres y como máximo 50 (o los que definamos nosotros) para ser válido. Si no es así la validación de Pydantic lanzará un error.
Si queréis más información acerca de las distintas validaciones que podéis utilizar según los tipos, os dejo el enlace a la doc de Pydantic.
Crear nuestro primer usuario
Ahora que ya tenemos un modelo para los usuarios, vamos a crear nuestro primer usuario. Para ello necesitaremos la información del usuario y un sistema para encriptar la contraseña. Recordad, nunca debemos tener en la base de datos las contraseñas de nuestros usuarios en texto plano.
Para encriptar la contraseña utilizaremos la librería passlib con el algoritmo de Bcrypt. Para instalarla lanzamos el siguiente comando:
pip install "passlib[bcrypt]"
Una vez instalada, vamos a ir al directorio /app/v1/service y crearemos un archivo llamado user_service.py con el siguiente contenido:
from fastapi import HTTPException, status
from passlib.context import CryptContext
from app.v1.model.user_model import User as UserModel
from app.v1.schema import user_schema
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def create_user(user: user_schema.UserRegister):
get_user = UserModel.filter((UserModel.email == user.email) | (UserModel.username == user.username)).first()
if get_user:
msg = "Email already registered"
if get_user.username == user.username:
msg = "Username already registered"
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=msg
)
db_user = UserModel(
username=user.username,
email=user.email,
password=get_password_hash(user.password)
)
db_user.save()
return user_schema.User(
id = db_user.id,
username = db_user.username,
email = db_user.email
)
En este archivo primero importamos HTTPException y status de FastAPI. La primera la usaremos cuando queramos lanzar una excepción controlada por FastAPI. Si lanzamos esta excepción, podremos customizar una respuesta para el usuario en formato JSON y con un código de estado de HTTP.
Status de FastAPI contiene los códigos de estado HTTP almacenados en constantes en el que en los nombres de las constantes tendremos información del significado del código de estado. Esto lo utilizaremos cuando queramos lanzar una excepción de tipo HTTPException.
Después importamos CryptContext que será la librería que emplearemos para encriptar las contraseñas.
Por último importamos nuestro modelo de usuario de peewee para poder crear el usuario y el modelo de usuario de Pydantic para retornar al usuario la información del usuario creado.
Ahora que hemos explicado los imports vamos con la siguiente parte del código. Primero creamos una instancia de CryptContext y posteriormente definimos una función llamada get_password_hash que encriptará la contraseña utilizando la instancia de CryptContext que acabamos de crear.
La siguiente función se llama create_user y recibe un modelo de Pydantic de tipo UserRegister. Esta función se encargará de guardar el usuario en la base de datos. Comprobamos si el usuario enviado ya existe en la base de datos por email o por username, si es así, lanzaremos una excepción HTTPException con el código de estado 400 y en el detail explicaremos el porqué del error.
Después usando el modelo de usuario de peewee, creamos el usuario con la contraseña encriptada y lo guardamos.
Por último retornamos la información del usuario recién creado empleando el modelo User de Pydantic.
Ruta para la creación de usuarios
Ya tenemos una función que nos permitirá crear usuarios, pero todavía no tenemos un endpoint que apunte a esta función. Don't worry porque lo vamos a crear ahora mismo.
Para realizar este paso, vamos a la carpeta /app/v1/router y generamos un archivo llamado user_router.py que contendrá el siguiente código:
from fastapi import APIRouter
from fastapi import Depends
from fastapi import status
from fastapi import Body
from app.v1.schema import user_schema
from app.v1.service import user_service
from app.v1.utils.db import get_db
router = APIRouter(prefix="/api/v1")
@router.post(
"/user/",
tags=["users"],
status_code=status.HTTP_201_CREATED,
response_model=user_schema.User,
dependencies=[Depends(get_db)],
summary="Create a new user"
)
def create_user(user: user_schema.UserRegister = Body(...)):
"""
## Create a new user in the app
### Args
The app can recive next fields into a JSON
- email: A valid email
- username: Unique username
- password: Strong password for authentication
### Returns
- user: User info
"""
return user_service.create_user(user)
Primero importamos APIRouter. Este nos permitirá crear rutas a nuestra API de forma separada del archivo main.py.
Luego importamos Depends y status que ya las conocemos y el último import que realizamos de FastAPI será Body. Utilizaremos esta función para recuperar la información que nos envíe el usuario en la petición para crear el usuario.
Seguidamente, importamos los modelos de Pydantic de usuarios y el servicio de usuarios que creamos anteriormente.
Por último, importamos la conexión a la base de datos.
Una vez hecho esto, generamos una instancia de la clase APIRouter con el parámetro prefix que será igual /api/v1. Esto significará que todas las rutas que creemos con esa instancia tendrán como prefijo esa url.
El segundo parámetro es tags que será una lista con un valor llamado users. A nivel de código no implica nada, pero nos servirá para agrupar nuestros endpoints por tipo cuando más adelante veamos el funcionamiento de la documentación que genera FastAPI automáticamente.
Ahora que ya tenemos la instancia de APIRouter configurada es hora de añadir nuestro primer endpoint. Para ello, al igual que cuando definimos el endpoint para retornar el mensaje "Hello world", creamos un decorador con router.
Indicamos que la petición será de tipo post usando él (valga la redundancia) método post de router y como parámetros recibirá varios parámetros.
El primer parámetro serla la ruta que nosotros la llamaremos "/user/" y que realmente como añadimos un prefijo en la instancia de APIRouter será /api/v1/user/.
El siguiente parámetro sería status_code que es el estado HTTP que queremos devolver en nuestro endpoint. Es un campo opcional y si no lo añadimos por defecto será el estado 200 OK, pero como lo que vamos a hacer es crear un dato, lo modificaremos y usaremos el estado 201.
El parámetro response_model indicará que la respuesta que retornaremos será un modelo de Pydantic de tipo User.
Como dependencies pasamos la conexión a la base de datos, ya que la necesitaremos para crear al usuario.
Y por último, el parámetro summary será informativo para la documentación.
Ahora que terminamos con el decorador, definimos la función. Esta recibirá una variable llamada user y que será de tipo UserRegister e igual a Body. Como cuando creamos los datos del modelo de Pydantic y Field, en el caso de que Body recibirá por parámetro "..." Significará que el campo es obligatorio.
Dentro de la función he definido un bloque de documentación. En este bloque podremos utilizar sintaxis de Markdown que FastAPI interpretará y podremos ver en la documentación. Yo lo he hecho con el formato que veis, pero podéis hacerla como queráis.
Al final llamamos a la función create_user que definimos en el archivo user_service.py y enviamos por parámetro la variable user. Esta retornará el modelo User de Pydantic que definimos en user_schema.py.
Lo siguiente que haremos serán modificaciones en el archivo main.py. Abrimos el archivo y realizamos los siguientes cambios:
from fastapi import FastAPI
from app.v1.router.user_router import router as user_router
app = FastAPI()
app.include_router(user_router)
El primer cambio que podéis ver es que hemos importado el router que acabamos de crear. Luego de instanciar FastAPI incluimos el router de usuario dentro de la app gracias al método include_router. Esto nos permitirá añadir rutas a nuestro proyecto desde otros archivos (como user_router).
Por último, he eliminado el endpoint que creamos de ejemplo, ya que no lo necesitaremos.
Listo, ya podemos probar nuestra API. Para ello levantamos el server si no lo hemos hecho y en nuestro navegador nos dirigimos a la siguiente url: http://127.0.0.1:8000/docs
Como podéis ver aparecerá la documentación de nuestra API con el endpoint que tenemos en este momento que es el endpoint para crear un usuario. Pues bien si desplegamos y pulsamos en Try it out, nos aparecerá un campo llamado Request Body que podremos editar para crear nuestro usuario.
Después solo debemos pulsar en Execute y listo, ya habremos creado nuestro primer usuario:
Perfecto, ya tenemos nuestro primer usuario y estamos listos para autenticarnos en nuestra API, pero eso lo veremos en el siguiente tutorial.
Como en tutoriales anteriores, os dejo repositorio por si necesitáis echarle un vistazo https://github.com/albertorc87/fastapi-todo-api/tree/tutorial-3-models-pydantic-create-user.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.