Definir un endpoint POST

En esta lección muestro cómo hacer un endpoint dentro de nuestro recurso que tiene la anotación POST para indicar que se debe ejecutar cuando la petición usa el verbo HTTP POST, y cómo podemos leer el payload mediante Jackson para convertirlo directamente a un objeto, como una clase. Quarkus también se va a ocupar de transformar el JSON de entrada en una instancia de la clase que le proporcionemos como parámetro.

Anotaciones de verbo

Hasta ahora solo hemos trabajado con la anotación GET, para definir endpoints que se ejecutan cuando se usa el verbo HTTP GET. Sin embargo, Quarkus, al igual que JAX-RS, tiene toda una colección de anotaciones que permiten declarar el verbo HTTP que queramos que se use para visitar esa ruta. Por ejemplo, @POST, @PUT, @PATCH, @DELETE...

Por poner un ejemplo, si a un endpoint le anotamos con un @POST, podemos hacer que ese endpoint se invoque cuando hacemos una petición a la URL utilizando el verbo HTTP POST. El siguiente ejemplo muestra una petición de este estilo:

package quarkus;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/temperaturas")
public class BasicTemperaturaResource {

    @POST
    public String unPost() {
        return "Esto es un POST";
    }

}

Sin embargo, para hacer una petición POST más atractiva, típicamente la vamos a asociar con la entrada de información. Buenas noticias: igual que podemos leer parámetros desde la querystring o desde los parámetros de ruta, podemos hacer que en los verbos que lo soportan, se pueda acoplar cuerpo a la petición y leerlo desde un parámetro. En particular, igual que Jackson puede serializar a JSON objetos, también puede convertir un JSON a un objeto de un tipo concreto.

A lo que quiero llegar con esto es que podemos agregarle a un endpoint POST un parámetro de un tipo que sea modelo, como nuestra Temperatura, y ya tendríamos todo hecho para poder leer payloads completos en peticiones de este tipo:

package quarkus;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/temperaturas")
public class BasicTemperaturaResource {

    @POST
    public Temperatura nueva(Temperatura temp) {
        return temp;
    }

}

El endpoint nueva() ahora acepta un parámetro de tipo Temperatura denominado temp. Este objeto queda definido por una clase cuyo código dejamos en el capítulo anterior de este curso, pero que se podría resumir en el siguiente record:

public record Temperatura(String ciudad, int minima, int maxima) { }

Si ahora nosotros hacemos una petición de tipo HTTP POST a /temperaturas y acoplamos un payload JSON, Jackson lo tratará de convertir a un objeto de tipo Temperatura:

POST /temps HTTP/1.1
Content-Length: 50
Content-Type: application/json
Host: localhost:8080

{
    "ciudad": "Lugo",
    "maxima": "22",
    "minima": "16"
}


HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 41

{
    "ciudad": "Lugo",
    "maxima": 22,
    "minima": 16
}

Lo que está ocurriendo aquí es que estamos mandando un payload JSON, que le entra como parámetro temp al método. El código del método ahora mismo lo único que contiene es un return, que lo devuelve tal cual le ha entrado. Por eso el endpoint nos devuelve con el mismo payload JSON que le hemos enviado. Sin embargo, entre medias ese JSON ha sido convertido a Temperatura durante la interpretación de la petición, y posteriormente a JSON otra vez en el proceso de generación de la respuesta.

Jackson no valida por defecto

Un detalle muy importante que tenemos que revisar en el futuro es el hecho deq ue Jackson no valida del todo los cuerpos que le pasemos. Por defecto, si le mandamos un documento JSON inválido con campos que no se corresponden con lo esperado, y donde faltan campos, esos campos simplemente estarán fijados a null. Observa que en el siguiente caso, le mandamos como cuerpo de la petición un documento JSON que contiene una key llamada "nombre" y hemos omitido el campo "ciudad". Por lo tanto, en la respuesta nos devuelve lo siguiente:

POST /temps HTTP/1.1
Content-Length: 50
Content-Type: application/json
Host: localhost:8080

{
    "maxima": "22",
    "minima": "16",
    "nombre": "Lugo"
}


HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 39

{
    "ciudad": null,
    "maxima": 22,
    "minima": 16
}

Si lo que le pasamos no es un documento JSON compatible, entonces sí pueden provocarse errores. En el siguiente ejemplo, mandamos como body JSON una cadena de caracteres. (¿Sabías que técnicamente hablando es un documento JSON válido, siempre que vaya escrito entre paréntesis?)

POST /temps HTTP/1.1
Content-Length: 45
Content-Type: application/json
Host: localhost:8080

"Este es un cuerpo de respuesta inapropiado"


HTTP/1.1 400 Bad Request
content-length: 0

Hagamos un pequeño CRUD en memoria

Con lo que hemos visto hasta ahora podemos fabricar un pequeño CRUD que guarde datos en una lista en memoria.

Si bien lo suyo es usar una base de datos para no tener problemas de estado con nuestra aplicación, a fines de ejemplo podemos mantener una lista en memoria instanciando un ArrayList en nuestro recurso, y usando el método POST que acabamos de fabricar para insertar la temperatura recibida en la lista de memoria. Luego, podemos hacer que el endpoint GET se use para recuperar los distintos elementos que formen parte de la lista en este instante. El código resultante sería el siguiente:

package quarkus;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/temperaturas")
public class BasicTemperaturaResource {

    private List<Temperatura> valores = new ArrayList<>();

    @GET
    public List<Temperatura> listar() {
        return Collections.unmodifiableList(valores);
    }

    @POST
    public Temperatura nueva(Temperatura temp) {
        valores.add(temp);
        return temp;
    }

}

Ten en cuenta que cada vez que guardes el archivo y provoques que quarkus reinicie su servidor web, la lista se vaciará. Esto es porque tiene que reiniciar la clase, lo que hace que se cree un nuevo estado completamente en blanco. Sin embargo, si no recargamos la clase y hacemos uso de los endpoints que hemos incorporado, podremos usar GET /temperaturas para ver la lista y POST /temperaturas para insertar una temperatura nueva.

Observa que si nada más cargar la clase visitas el endpoint con un verbo GET, la lista deberá estar vacía, pero a medida que insertes varias mediciones usando el verbo POST, el verbo GET ahora devolverá un array con más y más temperaturas.

Lista de reproducción
  1. 1
    Qué es Quarkus y cómo crear un proyecto
    8 minutos
  2. 2
    ¿Cómo crear endpoints en Quarkus?
    7 minutos
  3. 3
    Paso de parámetros con PathParam y QueryParam
    8 minutos
  4. 4
    Retorno de objetos JSON
    7 minutos
  5. 5
    Definir un endpoint POST
    7 minutos
  6. 6
    Servicios e inyección de dependencia
    8 minutos
  7. 7
    Response y ResponseBuilder
    8 minutos
  8. 8
    ExceptionMapper y tratamiento de errores
    9 minutos