Veremos cómo usar las variables que nos llegan por query string en NestJS para filtrar los datos que nos ofrecen los endpoints de nuestra API. Para ello usaremos DTO, validaciones y el método find() de TypeORM.
En el día de hoy en el Manual de Nest vamos a abordar un tema de uso muy frecuente en las aplicaciones web, para el que Nest nos ofrece una solución sencilla y ágil, como de costumbre. Se trata de una práctica en la que realizaremos búsquedas con TypeORM usando los datos que nos llegan desde el query string.
Usar el querystring es muy típico en las aplicaciones y APIs para refinar los resultados de las consultas que te ofrecen los endpoints. Por ejemplo, te damos algunos casos de uso frecuentes:
- Nuestro API puede dar la funcionalidad de decidir cuántos elementos deseas tomar de una entidad.
- Ofrecer la posibilidad de buscar elementos que respondan a una determinada consulta, por ejemplo que el nombre de un cliente contenga la palabra, desarrollo, de manera exacta o por parecidos.
- Que se ordene los resultados por un campo en particular, por ejemplo, la fecha
Para todos estos detalles sobre la consulta que deseamos componer lo más estándar en el mundo de las API REST es que usemos el endpoint GET sobre la raíz del recurso, enviando todos los datos de la consulta como variables en el query string.
Por ejemplo, si queremos que nos muestren únicamente 4 productos podríamos hacer una consulta como esta:
http://localhost:3000/products?limit=4
Si necesitamos que además ordene los datos por stock podríamos hacer una consulta como:
http://localhost:3000/products?limit=4&order=stock
Las consultas de esta manera pueden complicarse todo lo que sea necesario, usando un mismo endpoint del API, manejando siempre un juego de datos enviados, que generalmente son todos opcionales.
Recibiendo datos de la consulta en el controlador
Mediante el decorador @Query
podemos recibir todos los datos de la consulta. Vamos a comenzar con un ejemplo sencillo, que iremos luego complicando. En este ejemplo simplemente vamos a tener en cuenta la opción "limit" en el query string.
Para hacer un código robusto, tenemos que tener en cuenta un par de detalles:
- Limit es solo un dato meramente opcional. Por tanto tenemos que dar un valor predeterminado a la cantidad de elementos que nos devolverá la consulta si no lo indicamos.
- En todo caso es importante que tomemos la molestia de convertir el dato en entero, porque mediante query string siempre nos llegan cadenas. Pero además de convertir a entero es importante validarlo, porque podrían enviarnos algo en la variable limit que no sea exactamente un entero y no queremos que el API nos explote en la cara!
El código propuesto en esta primera aproximación sería el siguiente:
@Get()
async getAllProducts(@Query() query): Promise<Product[]> {
let limit = Number(query.limit) ?? 10;
if(isNaN(limit)) {
limit = 3;
}
return this.productsService.getAll(limit);
}
Como ves, una vez validado el valor limit, y definido el valor predeterminado, por si no lo mandan, podemos enviarlo al servicio para que se tenga en cuenta.
Contando con limit en el servicio
Luego, en el servicio tendríamos que componer correctamente la consulta con find()
, para tener en cuenta esta limitación en el número de registros a mostrar.
getAll(limit: number): Promise<Product[]> {
return this.productsRepository.find({
take: limit,
});
}
Recuerda que en un artículo anterior explicamos todas las opciones de configuración que podemos enviar al método find()
.
Manejando múltiples opciones en el Query String
Ahora pensemos en la posibilidad de que nos manden muchos datos para la consulta, como podrían ser:
- Limitación de los registros
- Definir la página que queremos consultar (para tareas de paginación)
- Definir el campo de orden
- Filtrar por nombre
Son tres ideas simples, pero podrían ser muchas más atendiendo a las necesidades de los clientes que van a consultar el API.
Lo que salta a la vista es que hacer todas las validaciones a mano sobre cada uno de los datos que podemos llegar a recibir puede ser demasiado laborioso. Así que vamos a hacer una estrategia de validación basada en pipes con ValidationPipe
.
Para aprender las bases de ValidationPipe, por favor consulta el artículo de validación en Nest.
Creando un DTO
Vamos a comenzar por crear un DTO para la validación de los datos del query string. Este DTO contiene unas reglas de validación que nos aporta class-validator
.
import { Type } from "class-transformer";
import { IsInt, IsOptional, IsString, Matches, } from "class-validator";
export class QueryProductDto {
@IsInt()
@Type(() => Number)
@IsOptional()
limit: number;
@Matches(/^(stock|name)$/)
@IsString()
@IsOptional()
order: string;
@IsString()
@IsOptional()
name: string;
}
Hemos validado lo siguiente:
- limit es un entero, gracias a que lo hemos convertido a Number con
@Type
. -
order
es una cadena que puede contener "stock
" o "name
", la coincidencia exacta. -
name
es un string libre. - Además, todas las propiedades son opcionales.
Recibiendo los datos en el controlador y seteando valores predeterminados
En el controlador podemos recibir todos los datos que queremos validar mediante @query
, igual que antes. Sin embargo, ahora vamos a ayudarnos del DTO para que se realicen las validaciones.
Adicionalmente, vamos a crear unas opciones predeterminadas y las mezclaremos con las opciones que nos lleguen en el objeto de query.
@Get()
async getAllProducts(@Query(new ValidationPipe()) query: QueryProductDto): Promise<Product[]> {
const mergedQuery = {
limit: 5,
order: 'name',
name: '',
...query
};
return this.productsService.getAll(mergedQuery);
}
Como puedes ver, gracias a ValidationPipe
conseguimos que se apliquen todas las reglas de validación de QueryProductDto
.
No te olvides de importar todas las declaraciones de los elementos que venimos usando!!
Usando el objeto query en el servicio
El uso del objeto de consulta dentro del servicio es bien sencillo, ya que hemos tomado la precaución de validarlo todo y aplicar valores por defecto a todas las propiedades de la consulta.
Así que solamente se trata de aplicar las propiedades correspondientes del objeto de opciones en find()
.
getAll(query: QueryProductDto): Promise<Product[]> {
return this.productsRepository.find({
take: query.limit,
where: {
name: Like(`%${query.name}%`),
},
order: {
[query.order]: 'ASC',
},
});
}
La verdad es que es sorprendente la cantidad de trabajo que te ahorra Nest para hacer validaciones y consultas sobre la base de datos.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...