Tu primera API com Spring Boot y Docker
¿Por qué usar Spring Boot?
Spring Boot es un framework de Java ampliamente usado para desarrollar aplicaciones web, especialmente las REST API.
Uno de los recursos más importantes que posee esta herramienta es su configuración automática muy al estilo Plug and Play.
Algunas de sus funciones:
- Manejo de dependencias.
- Tomcat, Jetty y Undertow integrados y disponibles de manera nativa.
- Disponibilidad de funciones como métricas, health checks, logs, etc.
- No requiere escribir código XML para configuraciones.
Esa última es una de mis preferidas. Si estás en el proceso de crear una aplicación Java web sin usar algún framework, sabes que deben atenderse varios detalles, entre ellos las propiedades y configuraciones en XML. Spring Boot abstrae esta complejidad al permitir que se realicen configuraciones a través de anotaciones de código.
Es importante resaltar también la facilidad para colocar las aplicaciones en producción. Como se mencionó anteriormente, métricas, health checks, logs y otras funciones están listas para uso. Asimismo, tenemos las bibliotecas que funcionan solo con añadir la dependencia en el archivo pom.xml. Construcción de APIs, bibliotecas para pruebas, seguridad y ORM están fácilmente disponibles.
¡Manos a la obra!
Supondré que ya entiendes lo básico de Java, algunos conceptos REST y de bases de datos relacionadas. Ahora que ya fueron detalladas las razones para usar Spring Boot, hablaremos un poco de la aplicación que vamos a construir con la herramienta.
El alcance de lo anterior será el siguiente: Nuestro cliente quisiera implementar una wishlist para los productos de su e-commerce. Con esa meta y conversando con él, escribimos esta lista de requisitos:
- El sistema necesita las entidades propietarias:
- Customer
- Product
- Wishlist
- Será necesario conservar la información en una base de datos MySQL.
- Las funciones para el consumidor serán:
- Registro en el sistema.
- Añadir un producto a su lista en dicho sistema.
- Tendremos un CRUD prácticamente completo para todas nuestras entidades.
- Contenedorización en Docker. El cliente necesita esto para implementar la aplicación más fácilmente.
Usaremos la versión de pruebas de IntelliJ IDEA Ultimate para codificar el proyecto. Puedes descargar la aplicación aquí.
Vamos a generar la estructura del proyecto con Spring Initializr. Esa herramienta crea todo el esqueleto de un proyecto Spring Boot según las dependencias deseadas.
Al abrir la página de la aplicación, completemos la información del lado izquierdo:
Aquí una breve descripción de todos los campos que completaremos/seleccionaremos:
- Project: Define el tipo de proyecto. Se refiere al Dependencies Manager que será usado. En este tutorial, trabajaremos con Maven Project.
- Language: Spring Boot permite el uso de tres lenguajes de programación. Java es el patrón y trabajaremos con él.
- Spring Boot: Relativa a la versión del programa. En esta oportunidad, avanzaremos con la última disponible: 2.7.1.
- Group: Dominio del paquete base de la aplicación.
- Artifact: Nombre de la aplicación.
- Description: Descripción de la aplicación.
- Package name: Paquete de Java principal de la aplicación.
- Packaging: Tipo de packaging que se empleará. La interfaz permite la selección de jar o war.
- Java: Versión de Java que se utilizará.
Esa es la primera etapa. Ahora elegiremos las dependencias:
Ingresa en add dependencies y busca cada una de las dependencias:
- Spring Data JPA: Reduce el código boilerplate necesario para el JPA que a su vez se ocupa de la complejidad de acceder y mapear entidades relacionales. Esa dependencia permitirá que no escribamos el código MySQL en la mano. El Hibernate que viene con él administrará las operaciones en nuestra base de datos a un nivel superior.
- Spring WEB: Usa MVC. REST y Tomcat como web server patrón.
- MySQL driver: Permitirá la conexión con la base de datos MySQL.
- Spring security: Empleada para definir el aceso a ciertas rutas de API.
- Lombok: Permite la reducción de código boilerplate repetitivo como getters, setters, construtores, etc.
Ahora solo necesitamos generar el proyecto haciendo clic en Generate y el zip se descargará en tu navegador.
Exporta el proyecto para el IntelliJ y ábrelo en la IDE:
Clic en Open y selecciona la carpeta zip (que acabamos de descargar) ya descomprimida.
IntelliJ abrirá y descargará todas las dependencias que seleccionamos para el proyecto.
Demos una mirada a algunas carpetas y archivos llave generados anteriormente, ahora abiertos en la IDE:
- Nuestro código se creará en el siguiente directorio: src/main/java/com/community/wishlist/
- En el mismo directorio tenemos el archivo Java src/main/java/com/community/wishlist/WishlistApplication.java el cual será el archivo maestro de nuestro proyecto. Éste será ejecutado cuando corramos nuestra aplicación. Ten en cuenta a @SpringBootApplication.
- Nuestras pruebas serán escritas en el directorio: src/test/java/com/community/wishlist
- El archivo pom.xml en la raíz del proyecto contiene información sobre el proyecto (llenamos varias en la sección project metadata en Spring Initilizr) así como las configuraciones para construir la aplicación. Actualmente, nuestro archivo luce de la siguiente forma:
En el futuro, éste se modificará para añadir las dependencias necesarias.
Crearemos todos nuestros modelos/entidades conforme a lo anteriormente planeado. Crea un nuevo paquete llamado model dentro de com.community.wishlist.
A continuación, crearemos nuestras clases Java para los modelos:
- Customer.
- Product.
- Wishlist.
¡Excelente! Ahora tenemos los siguientes archivos:
Los archivos están creados. Pese a que están todos iguales, sin sus atributos y comportamiento, continuaremos definiéndolos. Tendremos:
com/community/wishlist/model/Customer.java
Para el customer, es decir, el usuario que agregará un producto a su wishlist, tendremos los siguientes atributos principales:
- Id.
- Name.
- Email.
- Password.
Esas palabras clave son las anotaciones de Spring Boot que comentamos al inicio del texto.
- @Id: Indica que ese atributo es el identificador único del modelo.
- @GeneratedValue: Indica que ese atributo será generado.
Usaremos el Lombok que seleccionamos como una de nuestras dependencias:
Esa serie de anotaciones genera todo este código durante el tiempo de compilación:
Bien. Más simple y limpio usando Llombok, ¿verdad?
Para la clase Product tendremos los siguientes atributos:
- Id.
- Price.
- Brand.
- Title.
Vamos a crearlos en el archivo Product y definir ya las anotaciones de Lombok:
com/community/wishlist/model/Product.java
Para la clase Wishlist tendremos los siguientes atributos:
- Id.
- CustomerId.
- Products.
Los crearemos en el archivo Wishlist para definir ya las anotaciones de Lombok:
com/community/wishlist/model/Wishlist.java
¡Perfecto! Ahora tenemos creados todos nuestros modelos en la aplicación. Más tarde, haremos que hibernate cree todas las tablas automáticamente.
Creación del Repository
El siguiente paso es crear las clases de repository. Éstas serán responsables de abstraer el código necesario para las capas de acceso a los datos. De esta forma, el desarrollador no necesita escribir clases concretas con las implementaciones de sus métodos y ni siquiera crear una @Bean de Spring. Al implementar esa interfaz ya tenemos todas las funciones comunes en varias bases de datos como CREATE, READ, UPDATE y DELETE.
Requeriremos un repository para cada uno de nuestros modelos. Al final, haremos operaciones con todos ellos.
Usaremos el directorio src/main/java/com/community/wishlist/repository:
CustomerRepository.java
WishlistRepository.java
ProductRepository.java
Tratamiento de excepciones
Antes de definir una de nuestras nuevas capas, es preciso hablar un poco sobre el tratamiento de excepciones. En Spring Boot tenemos la opción de enganchar una excepción específica que definimos a un status de HTTP. Por ejemplo, puedes crear una clase de excepción llamada EntityAlreadyExistsException que se asociará al HTTP status code 422 (unprocessable entity). Usaremos esa excepción en el caso de un usuario que ya tiene un e-mail registrado intente registrarse de nuevo.
Crea el package com.community.wishlist.exception y, dentro de él, indica:
EntityAlreadyExistsException.java
Otra excepción que debemos considerar es aquélla para el caso de un recurso no encontrado. Ese caso será abordado cuando el usuario intente, por ejemplo, actualizar o leer una entidad que no exista.
Todavía necesitamos otros dos archivos, uno para representar nuestro modelo de respuesta cuando una de esas excepciones fuesen activadas y otro para hacer el wiring de nuestro error con la respuesta que dará Spring Boot si eso ocurre.
Nuestro modelo de respuesta será el siguiente:
ErrorResponse.java
Y nuestro ControllerAdvice:
GlobalExceptionHandler.java
En el archivo definimos la respuesta en caso de surgir alguna de las dos excepciones anteriormente creadas, sumada a la hipótesis de si una excepción genérica es activada.
¡Ahora tenemos nuestras excepciones listas para ser usadas en nuestra próxima capa!
La capa intermedia entre el endpoint y el la base de datos
En su momento creamos nuestros modelos y repositorios, pero ¿ingresaremos a los repositorios que acabamos de construir? Usaremos una capa de services que será llamada por el controlador. El primero a su vez llamará a los repositorios que creamos. Algo así:
Controller → Service → Repository
Vamos a comenzar declarando el servicio de customer en src/main/java/com/community/wishlist/repository/CustomerService.java:
Nota que estamos levantando nuestras excepciones creadas en el caso de que suceda un error específico (new ResourceNotFoundException("") e new ResourceNotFoundException("")).
Definimos en el archivo el CRUD de aplicación en nuestro servicio. En cada uno de los métodos, el servicio llama el repository para realizar las acciones en la base de datos.
Veamos las definiciones de las anotaciones que usamos:
- @Service: Indica que la clase hace operaciones basadas en la regla del negocio de aplicación. El spring automáticamente detectará cuándo hacer el classpath scanning.
- @Transactional: Indica que todo el método debe ser ejecutado de forma atómica.
En resumen, lo que hicimos fue:
- Definir el customerRepository que se usará para las operaciones de base de datos.
- Indicamos el constructor de clase con el customerRepository como argumento. Eso es importante para que Spring Boot inyecte esa dependencia en la clase.
- Precisamos las funciones:
create: crea un customer. Es el famoso regístrese en los sitios o plataformas.
readById: buscará al cliente por su id y lo devolverá.
update: actualizará el email y nombre del customer. El newCustomer como argumento es el objeto que fue enviado por el controller.
delete: elimina al customer de la base de datos.
Cada una de ellas llama el repository y realiza las operaciones en la base de datos.
Vamos a indicar también el ProductService:
y Ahora el WishlistService:
Controllers: la exposición de las rutas da aplicación
Como al ejecutar un PUT a localhost:8080/wishlist/1, ¿cómo se harían las adiciones de productos a la wishlist? Aquí es donde entran los controladores de aplicaciones. Vamos a crear un controlador para cada entidad, por lo que mantenemos las URL separadas respetando el marco REST.
Los próximos archivos serán creados en: src/main/java/com/community/wishlist/controller/
Generaremos el siguiente archivo y explicaremos su estructura y anotaciones enseguida:
CustomerController.java
- @RestController: Indica que trabajamos con un Controller. Combina las anotaciones @Controller y @ResponseBody. Asimismo, señala que ésta es la capa que controla las requisiciones. Es dirigida a quien debe responderlas.
- @RequestMapping: Infiere el mapeado HTTP para la data URL. en este caso, ese controller responderá al URL: nossoenderecoweb.com.br/customer/
- @PostMapping, @GetMapping, @PutMapping, @DeleteMapping: Indica la requisición usando el dato verbo (get, post, put, delete). Es decir, al enviar una requisición GET para el url nossoenderecoweb.com.br/customer/, el código de método getAll será ejecutado.
Como se puede percibir arriba, definimos el CRUD completo para la entidad Customer. La anotación hecha a la vinculación de operación HTTP con la función manejará esta solicitud y devolverá el resultado esperado.
Es importante notar que usamos la capa de service para acceder a la información de nuestro modelo. Nuestro controller no ingresa a la base de datos ni ejecuta reglas de negocio. Apenas recibe la requisición, llama a la capa responsable y responde lo necesario.
Es momento de definir nuestros otros controllers:
ProductController.java
WishlistController.java
Algunas configuraciones adicionales
Necesitamos definir algunas configuraciones para que nuestra aplicación funcione como deseamos. Podemos hacerlas en el archivo src/main/resources/application.properties. Tendremos lo siguiente:
Ya tenemos nuestras configuraciones de base de datos, Url, usuario y contraseña. Como también las configuraciones de hibernate, por cierto. Me gustaría destacar en especial al spring.jpa.hibernate.ddl-auto=update. La palabra clave update hará que hibernate compare nuestras entidades definidas en el package com.community.wishlist.model con lo que existe en nuestra base de datos. Mejor dicho: será esa keyword la que habilitará las modificaciones en nuestra base de datos cuando iniciemos la aplicación.
Swagger
Nuestra aplicación está lista para ejecutarse. Pero si no definimos ninguna respuesta para mostrar en nuestra URL raíz, mostrará la siguiente página:
Agregaremos a nuestro proyecto una página de documentación que permitirá que veamos todas las requisiciones, respuestas y entidades de nuestra aplicación.
Añade en el archivo pom.xml:
Crea un paquete com.community.wishlist.config.swagger en el archivo SpringFoxConfig.java:
Ese archivo hará todo el wiring con el spring para mostrar nuestra documentación que será generada automáticamente. Cuando ingresemos a http://localhost:8080/swagger-ui.html con el server up, veremos la siguiente página:
A todas éstas, ¡¿qué son containers?!
Se puede pensar en container como un lugar aislado que tu aplicación ejecuta. Casi como un sistema operativo que solo sirve para ejecutar tu aplicación. Es importante tener en cuenta que este "sistema operativo" contiene solo los archivos binarios y las dependencias necesarias para ejecutar su aplicación, lo que lo hace extremadamente liviano. Los contenedores comparten el mismo kernel que otros y se pueden iniciar en cuestión de segundos.
Debido a que este entorno está aislado, facilita que tu software se envíe con todas las dependencias necesarias que serán exactamente iguales (en versiones y librerías, por ejemplo). Esto reduce la fricción de entornos como desarrollo, control de calidad y producción (ese viejo problema de: “¡en mi máquina funciona!”). Con el contenedor, todas las computadoras tendrán exactamente el mismo entorno, con las mismas dependencias y binarios con las mismas versiones. Es como si ahora funcionara siempre, porque de forma simplificada es una simulación de la misma máquina que se puede ejecutar en cualquier lugar: en tu computadora, en la nube de tu compañero de trabajo o de la empresa.
Dockerizando nuestra aplicación
Bien, ahora insertemos nuestros archivos para que la aplicación esté en contenedores. Habrá dos:
- Dockerfile: contiene todas las instrucciones necesarias para subir nuestra imagen. Cuando se realice la construcción, todos los comandos en este archivo se ejecutarán automáticamente.
- Docker-compose.yml: nuestro archivo de inicio docker-compose. Es importante tener en cuenta que este archivo simple es nuestro orquestador de contenedores.
Nota que en nuestro archivo definimos dos servicios:
- mysql-service: nuestra base de datos mySQL que hibernate usará.
- En environment usamos las mismas credenciales del archivo application.properties
- web-service: nuestra API rest Spring boot
- Ella depende de que el servicio de MYSQL funcione antes que ella.
También tenemos la declaración y uso en los servicios de nuestra red que será la red interna de compose. Ten en cuenta que la URL de la base de datos en application.properties está formada por mysql-service. Esta configuración la realiza la red anterior.
Ambos se crearán en la carpeta raíz de la aplicación.
Ahora ejecuta el comando docker-compose up en tu terminal y verás cómo se activa toda nuestra aplicación y base de datos:
¡Ahora nuestra aplicación es funcional! Puedes ir a la página de Swagger e intentar ejecutar algunas operaciones.
Este es nuestro file tree final:
¡Y eso es todo! Acabas de construir una API en Java Spring Boot desde cero. Tenemos todas nuestras rutas funcionando para las entidades y el cliente estará feliz de que pudimos cumplir con todos los requisitos. ¡El sistema de wishlist permitirá a los clientes guardar sus productos deseados y volver más tarde para comprarlos!
Por supuesto, en el aspecto técnico podríamos haber hecho mucho más, pero este tutorial sería aún más largo. Una lista de posibles detalles que puedes implementar:
- Pruebas unitarias.
- Cambio de contraseña por correo electrónico al cliente usando spring-boot-starter-mail.
- Verificación de contraseña segura en el registro de clientes.
- Uso de DTO y clases de formulario para personalizar los campos recibidos y enviados en respuesta a las solicitudes.
- Rutas que requieren autenticación. Por el momento todas nuestras rutas son públicas.
- Imagina que pudiéramos recibir alguna información de una API existente de nuestro cliente, como productos. Entonces, ¿cómo crear un servicio que acceda a esta otra API para usar en la nuestra?
- Múltiples wishlists. Por el momento el usuario solo puede tener uno
Espero que te diviertas probando las sugerencias anteriores. Si deseas ver el código completo o simplemente descargarlo y ejecutarlo con un docker-compose up, entra en:
https://github.com/IanPedroV/wishlist
Revelo Content Network da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.