El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub springboot-demo-projects, bajo el tag first-endpoint.
La aplicación ahora inicia y responde con un 404 en cada ruta. Es hora de darle algo real que hacer. Este documento construye un único endpoint, GET /api/films/{id}, siguiendo arquitectura hexagonal desde el primer día. Los datos están hardcodeados por ahora; el objetivo es establecer la estructura en la que crecerá el resto de la aplicación.
El endpoint devuelve una película con este aspecto:
{ "id":42, "title":"ACADEMY DINOSAUR", "description":"A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies", "releaseYear":2006, "rating":"PG", "length":86, "language":"English" }
Todo lo nuevo vive bajo el feature film. La capa de dominio define el modelo y la interfaz del puerto. La capa adaptadora maneja las preocupaciones HTTP. Nada en el dominio sabe que HTTP existe.
Antes de escribir cualquier código, configura Spotless para aplicar un formato consistente en todo el proyecto. Ejecutar ./gradlew build formateará automáticamente tu código antes de compilar.
Los módulos Java y Groovy también necesitan un archivo greclipse.properties para configurar la indentación:
Con esto en su lugar, el formateo ya no es un paso manual ni una preocupación de revisión de código. Cada build produce código con formato consistente.
La capa de dominio es el corazón de la aplicación. Es dueña del modelo de negocio y declara qué operaciones están disponibles, sin saber nada sobre bases de datos, HTTP ni ningún otro detalle de infraestructura.
Film es el modelo de dominio central. Representa una película tal como la entiende la aplicación: no como una fila de base de datos, no como una respuesta HTTP, sino simplemente un objeto de datos plano.
package dev.pollito.spring_kotlin.sakila.film.domain.model dataclassFilm( val id: Int, val title: String, val description: String, val releaseYear: Int, val rating: String, val length: Int, val language: String, )
package dev.pollito.spring_groovy.sakila.film.domain.port.in import dev.pollito.spring_groovy.sakila.film.domain.model.Film interfaceFindByIdPortIn{ Film findById(Integer id) }
La interfaz vive en la capa de dominio. El controlador REST dependerá de esta interfaz, no de ninguna implementación concreta. Esa indirección es lo que hace que la arquitectura sea flexible.
FindByIdPortInImpl es la implementación de ese contrato. Por ahora, devuelve una película hardcodeada sin importar el ID proporcionado. La implementación real con soporte de base de datos llega en un documento posterior.
packagedev.pollito.spring_java.sakila.film.domain.port.in; importdev.pollito.spring_java.sakila.film.domain.model.Film; importorg.springframework.stereotype.Service; @Service publicclassFindByIdPortInImplimplementsFindByIdPortIn{ @Override publicFilmfindById(Integer id){ returnFilm.builder() .id(id) .title("ACADEMY DINOSAUR") .description( "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies") .releaseYear(2006) .rating("PG") .length(86) .language("English") .build(); } }
package dev.pollito.spring_kotlin.sakila.film.domain.port.`in` import dev.pollito.spring_kotlin.sakila.film.domain.model.Film import org.springframework.stereotype.Service @Service class FindByIdPortInImpl : FindByIdPortIn { overridefunfindById(id: Int): Film { returnFilm( id = id, title ="ACADEMY DINOSAUR", description = "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies", releaseYear =2006, rating ="PG", length =86, language ="English", ) } }
package dev.pollito.spring_groovy.sakila.film.domain.port.in import dev.pollito.spring_groovy.sakila.film.domain.model.Film import groovy.transform.CompileStatic import org.springframework.stereotype.Service @Service @CompileStatic classFindByIdPortInImplimplementsFindByIdPortIn{ @Override Film findById(Integer id){ newFilm( id: id, title:"ACADEMY DINOSAUR", description:"A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies", releaseYear:2006, rating:"PG", length:86, language:"English" ) } }
Los datos hardcodeados son intencionales. Permiten verificar que el ciclo completo de solicitud-respuesta funciona de extremo a extremo antes de introducir cualquier complejidad de persistencia.
La capa adaptadora traduce entre el dominio y el mundo exterior. Para un adaptador REST de entrada, eso significa recibir solicitudes HTTP, llamar al dominio y devolver respuestas HTTP.
FilmResponse es lo que la API expone a través de la red. En esta etapa refleja Film campo por campo, pero mantenerlos separados importa: el modelo de dominio puede evolucionar independientemente del contrato de la API.
package dev.pollito.spring_kotlin.sakila.film.adapter.`in`.rest.dto dataclassFilmResponse( val id: Int, val title: String, val description: String, val releaseYear: Int, val rating: String, val length: Int, val language: String, )
FilmRestMapper convierte un modelo de dominio Film en un DTO FilmResponse. Esta responsabilidad de traducción pertenece aquí, no en el controlador ni en el dominio.
El controlador depende únicamente de FindByIdPortIn (la interfaz) y FilmRestMapper. No tiene conocimiento de cómo está implementado FindByIdPortIn. Hoy son datos hardcodeados; más adelante será una consulta a base de datos. El controlador no cambiará en ninguno de los dos casos.
Aquí está el ciclo de vida completo de una solicitud:
Scroll to zoom • Drag corner to resize
Compila y ejecuta la aplicación, luego llama al endpoint:
Terminal
curl -s http://localhost:8080/api/films/42 | jq { "id": 42, "title": "ACADEMY DINOSAUR", "description": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies", "releaseYear": 2006, "rating": "PG", "lengthMinutes": 86, "language": "English" }
La arquitectura está en su lugar, pero FindByIdPortInImpl devuelve la misma película hardcodeada para cada ID. El siguiente paso es reemplazarla con una consulta real a la base de datos, lo que implica conectar un puerto secundario, un repositorio JPA y un mapper de entidad a dominio. La capa de dominio no cambiará en absoluto.