Implementación de una API REST completa (Parte I)

Marco Naranjo, Software Engineer en EDICOM, hace hincapié en la importancia de una API REST bien definida y nos explica cómo implementarla de forma que cumpla con los 4 niveles del modelo de madurez de Leonard Richardson. En esta primera parte del artículo, repasaremos los métodos GET y POST.

    Artículo elaborado por:

    Marco Naranjo

    Software Engineer

    Analista Programador especializado en Java y Python. Apasionado de los gráficos 3D y con gran interés en Machine Learning e Inteligencia Artificial.

Introducción

En la actualidad, el uso de microservicios está muy extendido dentro de las empresas tecnológicas más importantes del mundo. Estos se comunican a través de APIs que deben estar bien definidas para el correcto funcionamiento.

En el artículo de hoy hablaremos de cómo realizar una API REST que cumpla con los 4 niveles (del 0 al 3) del modelo de madurez de Leonard Richardson.

¿Qué es REST?

REST (Representational State Transfer) es un estilo de arquitectura de software propuesto por Roy Fielding en el año 2000 para diseñar servicios web para sistemas hipermedia distribuidos.

La principal ventaja de REST es que al utilizar HTTP/HTTPS no depende de la implementación de la API o de la aplicación del cliente, lo que permite una gran libertad a los desarrolladores.

Algunos de los principios de las APIs REST son:

  • Las API REST siempre se diseñan entorno a un recurso que será accedido por el cliente
  • Cada recurso tiene un identificador, este identificador es un URI único para cada recurso.
  • Los clientes interactúan intercambiando representaciones de los recursos.
  • Las API REST usan una interfaz uniforme que ayudan a desacoplar las implementaciones del cliente y del servicio. Las operaciones más comunes son GET, POST, PUT, PATCH y DELETE.
  • Las API REST no tienen estados y las peticiones pueden ocurrir en cualquier orden, por tanto, mantener el estado transitorio entre peticiones no es factible.
  • Las API REST se controlan por los links contenidos en la representación.

En 2008, Leonard Richardson propuso un nivel de madurez para las Web APIs:

  • Nivel 0: Define un URI, y todas las operaciones son peticiones POST a este URI.
  • Nivel 1: Crear URIs separados para cada recurso individual.
  • Nivel 2: Usar métodos HTTP para definir las operaciones en los recursos.
  • Nivel 3: Usar hipermedia (HATEOAS).

Normalmente, la mayoría de las API REST se estancan en el nivel 2, en este artículo vamos a lograr crear una API que llegue hasta el nivel 3.

Creando la API REST

Proyecto Inicial

Para no tener que empezar desde cero, os hemos preparado un proyecto en GitHub para que lo podáis descargar aquí.

Este proyecto contiene ya las dependencias necesarias para crear el proyecto completo, las que se necesitan de base están listas para ser usadas y las que no, se han dejado comentadas para una mayor comodidad. Además contiene una clase para indicar el formato de los errores (“ErrorDetails”), otra clase para el manejo de excepciones (“CustomResponseEntityExceptionHandler”), las clases con los repositorios JPA (“UserRepository” y “PostRepository”) y el par de Entidades con las que vamos a trabajar (“User” y “Post”).

Suponemos que vamos a crear una API para una aplicación similar a Twitter (bastante simplificada) para ello tendremos dos POJOs.

Tendremos los Posts, cada Post tendrá un ID, el usuario que lo escribió y el contenido del post, en este caso, es sólo texto.

Por otro lado tendremos el usuario que contendrá un ID, un nombre, una contraseña, una fecha de nacimiento y una lista de Posts relacionados, la relación entre User y Post es de uno a muchos.

Para tener datos con los que trabajar se ha creado dentro de la carpeta resources un fichero “data.sql” el cual inserta dos usuarios y un post para cada uno de ellos.

Dicho esto, comencemos.

Creación de la API

Para comenzar, debemos de crear un controlador el cual nos sirva para realizar las peticiones REST. Para ello vamos a crear dentro del paquete “user” una clase UserResource.

Esta clase debe estar marcada con @RestController pues aquí llegarán las peticiones. Como nuestros repositorios (es recomendable tener una clase intermedia service, pero en este caso nos sirve así) están definidos lo primero que vamos a hacer es crearnos dos variables, una para cada repositorio, y posteriormente un constructor que los reciba como parámetros, debería quedar algo así:

Implementación de una API REST completa
Los repositorios definidos y el constructor de la clase

Una vez tengamos los repositorios vamos a crear los distintos métodos que manejan las peticiones. A la hora de crear estos métodos nos hemos basado en la especificación de Microsoft.

El método GET

El método GET se utiliza únicamente para consultar información al servidor, sería parecido a hacer un “SELECT” en base de datos. Dentro de la petición tenemos que tener en cuenta varios aspectos importantes:

  • No soporta payload en la petición. Sólo contiene los headers y parámetros de la consulta.
  • Es uno de los métodos más seguros pues se usa para recuperar información, no modifica datos en el servidor.
  • Su respuesta normalmente suele devolverse en JSON o XML.

Para elegir correctamente qué URL debemos usar tenemos que tener en cuenta que cada recurso tiene que tener su URI único, y que se suele recomendar usar nombres (y evitar verbos) que identifiquen al recurso en plural pues ayudan a identificar más sencillamente los recursos, por ejemplo:

/users – Devuelve todos los usuarios

/users/id – Devuelve el usuario con ese id

Mientras que si se usara en singular, perdería un poco el sentido. Lo mismo sucede con los verbos, si estamos haciendo una petición GET para recoger todos los usuarios no tiene sentido que la URL indique “/getUsers” pues es redundante.

Implementación de los métodos GET

GET – /users

Para implementar el método que recoja todos los usuarios de forma muy sencilla podríamos hacer algo así:

Implementación de una API REST completa
Primera implementación para encontrar todos los usuarios

@GetMapping se encarga de indicar el path al recurso e indicar que es una petición REST mientras que el repositorio recoge todos los usuarios, esto funciona y es correcto pues devuelve el estado 200 si los recoge y 404 si no encuentra pero se puede mejorar.

El método actualmente devuelve las contraseñas de todos los usuarios y aunque se guardan encriptadas es mejor no exponerlas. Para ello vamos a filtrar dinámicamente el objeto, para ello vamos a seguir estos pasos:

Primero, descomentamos la anotación @JsonFilter de la clase User:

Implementación de una API REST completa
@JsonFilter descomentado en la clase User

Segundo, vamos a recoger todos los usuarios y vamos a guardarlos dentro de un objeto MappingJacksonValue:

Implementación de una API REST completa
Recogemos todos los usuarios para guardarlos en el MappingJacksonValue

Este objeto todavía contiene la contraseña, por tanto vamos a filtrar aplicando un filtro que quite todas las propiedades excepto las indicadas:

Especificamos las propiedades y añadimos el filtro

Aplicamos el filtro y podemos devolver el objeto:

Implementación de una API REST completa
Aplicamos el filtro y devolvemos el objeto

Así quedaría la función al completo:

Implementación de una API REST completa
Función para encontrar todos los usuarios al completo

Podemos realizar la petición con algún programa como Postman o en nuestro caso, utilizaré una extensión de Chrome llamada “Talend API Tester” (es gratuita). Así quedaría la petición:

Implementación de una API REST completa
Ejecución correcta de la petición GET «/users»

Importante: el puerto se puede configurar yo he usado el 8081, pero por defecto usa el 8080.

GET – /users/{id}

Ahora vamos a recoger un único usuario, filtrando por su ID. Para realizarlo como antes la forma más básica sería algo así:

Implementación de una API REST completa
Filtro por ID sencillo

Detalle a destacar el id como @PathVariable para que se recoja el ID de la URL.

Esta petición tiene varios problemas, como antes está devolviendo contraseñas, debemos filtrarlo, además no está siguiendo la especificación pues no devuelve el estado 404 si no lo encuentra, estaríamos devolviendo un 500 y por último, si queremos usar HATEOAS (explicaremos lo que es, algo más abajo) el usuario debe devolver la URL que identifica a todos los usuarios.

Conseguir que devuelva 404 cuando no encuentra un usuario se puede conseguir de distintas maneras, en este caso, vamos a utilizar una excepción personalizada.

Para ello vamos a crear dentro del paquete “user” una clase sencilla que extienda RuntimeException llamada “UserNotFoundException”.

Dentro de esta clase solo necesitamos un constructor y que sobre el encabezado de la misma le indiquemos el estado que debe devolver con @ResponseStatus(code = HttpStatus.NOT_FOUND). Debería de quedar así:

Implementación de una API REST completa
Excepción para cuando no encontremos los usuarios

Lanzaremos esta excepción si no encontramos el usuario indicado y la manejaremos dentro de nuestra clase “CustomResponseEntityExceptionHandler”. Para ello vamos a añadir este método a esa clase:

Implementación de una API REST completa
Método para tratar las excepciones de usuario

Este método recogerá las excepciones indicadas en @ExceptionHandler y las tratará como las indiquemos, en este caso, creamos un objeto ErrorDetails y luego le ponemos el estado 404.

Es el momento de explicar qué es HATEOAS. HATEOAS es un acrónimo de Hypermedia As The Engine Of Application State, y es una de las características más significativas de REST, pues REST persigue la intención de presentar una interfaz universal. La intención de HATEOAS es que el cliente pueda moverse entre recursos de forma sencilla, proporcionando para cada recurso una lista de URIs relacionados con el mismo. De esta forma, se suelen devolver dentro del JSON los URIs necesarios.

Spring nos aporta varias dependencias útiles para esto, usaremos sólo “spring-boot-starter-hateoas” pero también es interesante “spring-data-rest-hal-explorer” pues aporta un explorador de URIs para el cliente.

Vamos a ir al POM y a descomentar “ spring-boot-starter-hateoas”.

Os añadimos una captura de cómo debería ser la función sin añadir el HATEOAS añadiendo la excepción si no encontramos el usuario y el filtro de la contraseña:

Implementación de una API REST completa
Función al completo sin HATEOAS

Ahora configuremos los links de HATEOAS.

Primero vamos a crearnos un objeto EntityModel de usuario con el usuario recogido:

Implementación de una API REST completa
Recogemos el usuario del Optional

Después de esto, tenemos que hacernos con el link hacia “/users”, como no es buena idea indicar el link directamente por código, vamos a obtenerlo de las funciones de WebMvcLinkBuilder, debemos importar de manera estática tanto linkTo, como methodOn.

Implementación de una API REST completa
Recogemos el URI de la llamada

Por último, añadimos el link al entityModel con el nombre que queramos, en nuestro caso “all-users”.

Implementación de una API REST completa
Añadimos el nombre para el URI anterior

Es importante cambiar el objeto que pasamos al MappingJacksonValue:

Implementación de una API REST completa
Cambiamos el objeto que recibe el MapppingJacksonValue

La función entera se vería así:

Implementación de una API REST completa
Método para recuperar un usuario completo

Esta sería la función y aquí la petición (y respuesta) del usuario 1001:

Implementación de una API REST completa
Petición y respuesta para el usuario 1001

Como podéis ver, sin contraseñas y con el link que lleva a todos los usuarios.

El método POST

El método POST se utiliza para enviar información al servidor. Dentro de la petición tenemos que tener en cuenta varios aspectos importantes:

  • Debemos enviar en el body los datos a enviar al servidor, normalmente en JSON o XML.
  • Se suele utilizar para crear nuevos recursos o actualizarlos, por tanto, se debe tener cuidado con su uso.
  • Debe devolver un estado correspondiente indicando si se ha completado o no la solicitud.

Implementación del método POST

En nuestro caso, sólo tendremos un método POST para crear un usuario. Este método debe usar el mismo path (/users) que el método para recoger todos los usuarios, al cambiar el tipo del método sabremos si estamos recogiendo usuarios usando get o si creando usando post.

Sabiendo que podemos crear un usuario con userRepository.save(user), el método es sencillo, pero esta vez vamos a aplicar un filtro vacío, pues tenemos que devolver el usuario creado incluida la contraseña pues es un dato que la persona que ha hecho la petición nos ha proporcionado. Os dejo como se vería la función:

Implementación de una API REST completa
Método inicial para crear usuarios sin filtrar

La anotación @PostMapping es la que se encarga de que la llamada sea un método POST con ese path.

Cabe destacar el @RequestBody en el que indicamos que esperamos en el body un usuario y que en el filtro no le hemos pasado parámetros para que no filtre nada.

La especificación dice que debemos devolver el código 201 cuando creemos un usuario y el URI de ese usuario creado dentro del header “location”, un 400 si la petición es errónea (esto ya lo hace Spring por defecto) o un 204 si no devolvemos el objeto en el body(que no es nuestro caso pues devolveremos un usuario).

¿Cómo hacemos para devolver un 201 con el URI en el header location? Vamos a ello.

Primero tendremos que cambiar la respuesta de nuestro método a un ResponseEntity de MappingJacksonValue:

Implementación de una API REST completa
Cambiamos la respuesta a un ResponseEntity de MappingJacksonValue

Hemos añadido un @Valid para que todas las validaciones que hemos puesto en User se verifiquen cuando nos pasen el objeto.

Implementación de una API REST completa
Validaciones de la clase User

Ahora vamos a la función de nuevo.

Vamos a guardar el usuario que se nos ha pasado, esto nos devolverá el usuario que acabamos de crear:

Implementación de una API REST completa
Guardamos y recogemos el usuario

A este usuario le aplicamos un filtro vacío pues podemos devolver la contraseña, ya que nos la tienen que pasar para crear el usuario:

Implementación de una API REST completa
Usamos un filtro vacío para el usuario

Ahora necesitamos el URI del recurso, para ello vamos a recoger el URI de la petición actual y le sumaremos el ID del usuario que acabamos de recoger con la ayuda de ServletUriComponentsBuilder.

Implementación de una API REST completa
Recogemos el URI y le añadimos el ID del Usuario

Por último, tenemos que devolver un 201 (Created) con el header location y el objeto en el body, esto es sencillo.

Implementación de una API REST completa
Devolvemos 201 con URI Location y el objeto en el body

La función quedaría así:

Implementación de una API REST completa
Método al completo para crear usuarios

La petición sería:

Implementación de una API REST completa
Petición y su respuesta. Se puede ver el header Location bien definido

Como se puede observar, devuelve el usuario y la URL del mismo en el header location.

Conclusión

En este artículo hemos cubierto los métodos GET y POST en la implementación de una API REST. Hemos visto cómo el método GET se utiliza para obtener recursos de un servidor y cómo el método POST se utiliza para enviar datos y crear nuevos recursos.

Es importante tener en cuenta que GET y POST son solo dos de los muchos métodos disponibles en una API REST completa. En la segunda parte de este artículo, exploraremos los métodos PUT, PATCH y DELETE, que son igualmente importantes para la implementación de una API REST robusta.

Con estos métodos, podemos actualizar y eliminar recursos, lo que es fundamental para cualquier API que maneje datos dinámicos.

En resumen, si bien hemos cubierto una buena cantidad de información importante en este artículo, aún hay mucho por explorar en cuanto a la implementación de una API REST completa y efectiva. Por lo tanto, te invitamos a seguir leyendo la segunda parte de este artículo, donde abordaremos más métodos REST y cómo se pueden utilizar en conjunto para crear una API REST exitosa.