🚀 Guía práctica para construir APIs RESTful con Spring Boot
22 minutos de lectura
📚 Contenido
Actualización [17/06/2025]: Este artículo ha sido actualizado para incluir una sección de primeros pasos.
Actualización [16/06/2025]: Este artículo ha sido actualizado para una sección avanzada sobre mejores prácticas con DTOs y separación de responsabilidades, junto con una conclusión más detallada sobre los beneficios de esta arquitectura.
Spring Boot se ha convertido en uno de los frameworks más populares para desarrollar aplicaciones Java modernas. Su enfoque centrado en la simplicidad y la configuración mínima lo hace ideal para construir APIs REST de forma rápida y robusta. En esta guía te mostraré cómo manejar rutas, controlar peticiones y respuestas, y estructurar tu aplicación siguiendo buenas prácticas. ¡Comencemos!
🌱 ¿Qué es Spring Boot?
Es una extensión opinada de Spring que:
- Simplifica la configuración eliminando el “boilerplate”.
- Incluye starters: dependencias que ya agregan todo lo necesario (ej.: spring-boot-starter-web, spring-boot-starter-data-jpa, etc.)
- Incorpora un servidor embebido (Tomcat, Jetty…) para ejecutar aplicaciones como jar ejecutables
- Ofrece autoconfiguración que detecta lo que tienes en el classpath y activa automáticamente lo pertinente
- Trae características “lista para producción”: Actuator (métricas, health checks), configuración externa, logging integrado
🚶Primeros pasos para crear la aplicación (Nuevo: 17/06/2025)
Primero que todo vamos a ir a Spring Initializr. Lo podemos buscar en Google o ir directamente a través de la barra de direcciones e ingresar https://start.spring.io/
Nos va a aparecer una página como esta:
En esta guía seleccionaremos las opciones que aparecen en la imagen y debemos completar los datos del formulario Project Metadata, pueden reemplazar los datos de ejemplo por los que quieran:
Luego vamos a agregar las dependencias:
Vamos a agregar la dependencia Spring Web:
Luego agregamos Lombok:
Nuestras dependencias agregadas quedarán listadas de la siguiente forma:
Finalmente generamos el proyecto haciendo click en el botón “Generate”.
Ese botón nos descargará el proyecto comprimido en un .zip
Abrimos la carpeta que contiene el archivo y la movemos a nuestra carpeta de workspace para a continuación extraer la carpeta.
En esta guía utilizaremos NetBeans, pero podemos usar otros IDEs como Eclipse con Spring Tools (STS), IntelliJ IDEA o editores de texto cómo Visual Studio Code que también tiene extensiones muy útiles como Spring Boot Extension Pack.
Abrimos NetBeans, click en “File” y luego “Open Project…”
Aparecerá una ventana, buscamos el archivo, lo seleccionamos y hacemos click en “Open Project”.
Podemos ver que tenemos una clase llamada FirstApiApplication que tiene un método llamado main. Esta es nuestra clase principal.
Vamos a dar la instrucción para que NetBeans sepa que esta es la clase principal. Para esto en el proyecto hacemos click derecho, click en “Properties”:
Click en “Run” y luego en “Browse…”
Aparecerá una ventana “Browse Main Classes”, seleccionamos la clase, click en “Select Main Class” y click en el botón “OK”.
Con esto ya tenemos configurado el proyecto y podemos iniciar el servidor web (Apache Tomcat). Para esto vamos a hacer click en el botón de play.
Al hacer click veremos que aparecerá una terminal en la parte inferior del IDE y veremos el log de cómo comienza a iniciar el servidor web. Cuando termine de iniciar el servidor web veremos el mensaje “Tomcat started on port 8080 (http) with context path ‘/’”. Esto nos indica que el servidor inició en el puerto 8080.
Ya podemos ir al navegador e ingresar en la barra de direcciones http://localhost:8080. Veremos un error, pero esto es completamente normal ya que sólo hemos creado el proyecto base para trabajar, aún no hemos programado ningún endpoint.
🏷️ ¿Qué son las anotaciones en Spring?
Las anotaciones en Java (y en Spring) son marcas especiales que comienzan con @
y se colocan sobre clases, métodos o atributos para darle instrucciones al framework sobre cómo debe comportarse ese elemento.
En el contexto de Spring, las anotaciones reemplazan mucha configuración manual (como XML) y hacen el código más limpio y fácil de mantener.
🔧 Ejemplos comunes de anotaciones en Spring Boot
Anotación | ¿Para qué sirve? |
---|---|
@SpringBootApplication |
Marca el punto de entrada principal de una app Spring Boot |
@Component |
Indica que una clase es un componente administrado por Spring |
@Service |
Similar a @Component , pero semánticamente indica lógica de negocio |
@Repository |
Marca una clase que accede a la base de datos |
@Controller |
Define una clase que maneja peticiones HTTP |
@Autowired |
Inyecta automáticamente dependencias |
📦 ¿Cómo se importan?
En la mayoría de los entornos de desarrollo como IntelliJ IDEA o Eclipse, cuando escribes una anotación, el IDE suele sugerir automáticamente el import correcto.
Pero también puedes importarlas manualmente:
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
💡 Estas anotaciones provienen de distintos paquetes dentro de Spring. Por ejemplo:
@Component
,@Service
,@Repository
: vienen deorg.springframework.stereotype
@Autowired
: viene deorg.springframework.beans.factory.annotation
@SpringBootApplication
: viene deorg.springframework.boot.autoconfigure
🌐 Anotaciones para controladores REST en Spring
Las anotaciones en los controladores REST de Spring Boot permiten definir de manera sencilla cómo se manejan las rutas y cómo se recibe la información del cliente. Esto hace que la creación de endpoints sea clara, flexible y fácil de mantener.
Anotaciones para rutas según el verbo HTTP
Verbo HTTP | Anotación | Uso común |
---|---|---|
GET | @GetMapping |
Obtener recursos |
POST | @PostMapping |
Crear nuevos recursos |
PUT | @PutMapping |
Reemplazar recursos |
DELETE | @DeleteMapping |
Eliminar recursos |
PATCH | @PatchMapping |
Actualización parcial |
Anotaciones para recibir datos del cliente
Estas anotaciones permiten a los controladores recibir información enviada por el cliente de distintas formas, facilitando la construcción de endpoints flexibles.
Anotación | Uso principal |
---|---|
@PathVariable |
Extrae datos directamente de la ruta URL |
@RequestBody |
Indica que un parámetro del método viene del cuerpo HTTP |
@PathVariable
se utiliza para capturar valores dinámicos que forman parte de la URL (por ejemplo, un identificador en /api/tareas/123
), mientras que @RequestBody
permite recibir y deserializar datos enviados en el cuerpo de la petición, como objetos JSON en operaciones POST o PUT.
Estas herramientas facilitan la construcción de endpoints flexibles y adaptados a diferentes necesidades de entrada de datos.
💡 Todas estas anotaciones pertenecen al paquete org.springframework.web.bind.annotation
, el cual es esencial para construir controladores REST con Spring MVC o Spring Boot.
Importación
Podemos importar cada anotación de forma individual o usar el comodín para importar todas:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
// o simplemente
import org.springframework.web.bind.annotation.*;
Buenas prácticas
- Utiliza DTOs para separar la lógica de tu dominio del formato de entrada/salida.
- Valida los datos recibidos en el cuerpo de la petición usando anotaciones como
@Valid
. - Mantén tus controladores enfocados solo en la lógica de manejo de peticiones.
Error común
- No validar los datos recibidos, lo que puede provocar errores en tiempo de ejecución.
Ejemplo completo: CRUD de tareas 🗂️
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
@Autowired
private TaskService taskService;
// GET: obtener una tarea
@GetMapping("/{id}")
public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id)
.map(task -> new ResponseEntity<>(task, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
// POST: crear una tarea
@PostMapping
public ResponseEntity<Task> createTask(@RequestBody Task task) {
Task created = taskService.saveTask(task);
return new ResponseEntity<>(created, HttpStatus.CREATED);
}
// PUT: actualizar una tarea completamente
@PutMapping("/{id}")
public ResponseEntity<Task> updateTask(@PathVariable Long id, @RequestBody Task task) {
return new ResponseEntity<>(taskService.updateTask(id, task), HttpStatus.OK);
}
// PATCH: actualizar parcialmente una tarea
@PatchMapping("/{id}")
public ResponseEntity<Task> patchTask(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
return new ResponseEntity<>(taskService.patchTask(id, updates), HttpStatus.OK);
}
// DELETE: eliminar una tarea
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
🔍 Parámetros dinámicos y consultas personalizadas
Ya sea extrayendo valores de la URL con @PathVariable
o leyendo parámetros de consulta con @RequestParam
, Spring Boot te da herramientas claras para manejar solicitudes complejas.
Ejemplo con parámetros de búsqueda:
@GetMapping("/search")
public ResponseEntity<List<Task>> searchTasks(
@RequestParam String title,
@RequestParam(required = false) Boolean completed
) {
List<Task> tasks = taskService.findByTitleAndStatus(title, completed);
return new ResponseEntity<>(tasks, HttpStatus.OK);
}
🧱 Separación por capas: arquitectura limpia en Spring Boot
Para un desarrollo escalable, aplica una arquitectura multicapa:
- Controller: entrada de peticiones HTTP.
- Service: lógica de negocio.
- Repository: acceso a base de datos.
- DTO/Model: transporte de datos y mapeo a entidades.
com.example.app
├── controller
├── service
├── repository
├── dto
└── model
💾 Persistencia con JPA + Hibernate
Spring Boot configura automáticamente JPA (usualmente con Hibernate) para que puedas mapear objetos Java a tablas SQL fácilmente:
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
}
Y para configurar la base de datos, basta con esto en application.properties
:
spring.datasource.url=jdbc:mysql://localhost:3306/taskdb
spring.datasource.username=root
spring.datasource.password=tu_clave
spring.jpa.hibernate.ddl-auto=update
📦 Uso de DTOs para separar entidades y transporte de datos
Un DTO (Data Transfer Object) es una clase que se utiliza para transportar datos entre el cliente y el servidor, sin exponer directamente las entidades del dominio.
Ventajas de usar DTOs
- 🔒 Ocultas campos sensibles o irrelevantes de la entidad.
- 🧩 Puedes personalizar qué datos enviar o recibir según el caso de uso.
- 🔄 Desacoplas la lógica de persistencia del formato de comunicación.
Ejemplo de DTO para crear tareas:
public class TaskCreateDTO {
private String title;
private boolean completed;
// Getters y setters
}
Y para la respuesta:
public class TaskDTO {
private Long id;
private String title;
private boolean completed;
public static TaskDTO fromEntity(Task task) {
TaskDTO dto = new TaskDTO();
dto.setId(task.getId());
dto.setTitle(task.getTitle());
dto.setCompleted(task.isCompleted());
return dto;
}
}
🧪 Inyección de dependencias en Spring Boot
La inyección de dependencias (DI) permite a Spring proporcionar automáticamente los objetos que una clase necesita, sin que ella los cree manualmente.
Ventajas:
- 🔄 Menor acoplamiento entre clases.
- 🧪 Facilita las pruebas unitarias.
- ♻️ Mayor reutilización y mantenibilidad del código.
Ejemplo:
@Service
public class TaskService {
private final TaskRepository taskRepository;
@Autowired // Constructor recomendado
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
public Task saveTask(Task task) {
return taskRepository.save(task);
}
}
🗃️ Acceso a datos con Repository
La anotación @Repository
marca una interfaz como componente que accede a la base de datos. Usando JpaRepository
, heredas métodos CRUD listos para usar.
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
List<Task> findByCompleted(boolean completed);
}
🔗 Llevando todo a la práctica: POST con DTO + DI + Repository
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
@Autowired
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping
public ResponseEntity<TaskDTO> createTask(@RequestBody TaskCreateDTO taskDto) {
Task task = new Task();
task.setTitle(taskDto.getTitle());
task.setCompleted(taskDto.isCompleted());
Task saved = taskService.saveTask(task);
return new ResponseEntity<>(TaskDTO.fromEntity(saved), HttpStatus.CREATED);
}
}
Ventajas de esta implementación
- ✅ DTO protege tu modelo interno y simplifica la comunicación con el cliente.
- ✅ Inyección de dependencias facilita pruebas y mantiene tu código limpio.
- ✅ Repository centraliza el acceso a la base de datos, usando interfaces concisas.
🔁 Refactor: del ejemplo simple a una arquitectura limpia (Nuevo: 16/06/2025)
El ejemplo anterior funciona bien, pero aún mezcla responsabilidades dentro del controlador (como la creación manual de la entidad Task
). A continuación, veremos cómo aplicar una arquitectura más limpia mediante separación de capas, uso de DTOs especializados, mapeadores dedicados y validación declarativa.
TaskCreateDTO.java
import jakarta.validation.constraints.NotBlank;
public class TaskCreateDTO {
@NotBlank(message = "El título es obligatorio")
private String title;
private boolean completed;
// Getters y setters
}
TaskDTO.java
public class TaskDTO {
private Long id;
private String title;
private boolean completed;
// Constructor
public TaskDTO(Long id, String title, boolean completed) {
this.id = id;
this.title = title;
this.completed = completed;
}
public static TaskDTO fromEntity(Task task) {
return new TaskDTO(task.getId(), task.getTitle(), task.isCompleted());
}
// Getters y setters (si usas Lombok puedes evitarlos)
}
TaskMapper.java
import org.springframework.stereotype.Component;
@Component
public class TaskMapper {
public Task toEntity(TaskCreateDTO dto) {
Task task = new Task();
task.setTitle(dto.getTitle());
task.setCompleted(dto.isCompleted());
return task;
}
public TaskDTO toDto(Task task) {
return new TaskDTO(task.getId(), task.getTitle(), task.isCompleted());
}
}
TaskService.java
import org.springframework.stereotype.Service;
@Service
public class TaskService {
private final TaskRepository taskRepository;
private final TaskMapper taskMapper;
public TaskService(TaskRepository taskRepository, TaskMapper taskMapper) {
this.taskRepository = taskRepository;
this.taskMapper = taskMapper;
}
public TaskDTO saveTask(TaskCreateDTO dto) {
Task task = taskMapper.toEntity(dto);
Task saved = taskRepository.save(task);
return taskMapper.toDto(saved);
}
}
TaskController.java
(refactorizado)
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@PostMapping
public ResponseEntity<TaskDTO> createTask(@RequestBody @Valid TaskCreateDTO taskDto) {
TaskDTO saved = taskService.saveTask(taskDto);
return new ResponseEntity<>(saved, HttpStatus.CREATED);
}
}
ControllerAdvice
para manejar errores de validación (opcional pero recomendado)
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
Beneficios del cambio
Mejora | Justificación |
---|---|
Separación de responsabilidades | El controlador solo orquesta, no construye entidades. |
Validación declarativa | El DTO tiene validaciones con @NotBlank . |
Mapper dedicado | Se estandariza la transformación entre DTOs y entidades. |
Más testable | Cada clase tiene una única responsabilidad, facilitando los tests unitarios. |
¿Por qué usar TaskDTO y TaskCreateDTO?
Clase | Uso | Contenido típico |
---|---|---|
TaskCreateDTO |
Entrada (request) para crear tareas | Solo campos que el cliente puede enviar (ej: title , completed ) |
TaskDTO |
Salida (response) para mostrar tareas | Campos calculados o internos: id , createdAt , updatedAt , etc. |
📦 Despliegue con Docker 🐳
Ejemplo de Dockerfile para construir tú imagen:
FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]
Ejemplo de configuración de Docker Compose:
version: '3'
services:
app:
build: .
ports:
- '8080:8080'
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/taskdb
depends_on:
- db
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=taskdb
🧠 Conclusión
Spring Boot es una herramienta poderosa para desarrollar APIs REST modernas y bien estructuradas. Gracias a su conjunto de anotaciones como @GetMapping
, @PostMapping
, @PutMapping
, @DeleteMapping
y @PatchMapping
, puedes construir endpoints eficientes de manera declarativa y clara 🧩. Al combinarlo con una arquitectura por capas, patrones como DTO y prácticas como el despliegue con Docker, obtienes una solución completa y lista para producción 🚀. Los principales beneficios de los temas abordados en esta guía son:
🔧 1. Separación de responsabilidades (SRP)
- El controlador (TaskController) ya no construye entidades, solo orquesta el flujo.
- La lógica de negocio y persistencia se delega al TaskService.
- La transformación entre DTOs y entidades queda encapsulada en TaskMapper.
Beneficio: más limpio, más testable y cada clase hace solo lo que le corresponde.
📦 2. Uso de DTOs
- Se utilizaron TaskCreateDTO para entradas (POST) y TaskDTO para salidas (responses).
- Esto protege la entidad interna (Task) de exposiciones innecesarias y errores de seguridad.
Beneficio: puedes controlar qué campos expone o recibe la API, manteniendo coherencia y evitando sobreexposición de datos.
🧹 3. Validación declarativa
- Uso de anotaciones como @NotBlank, @Valid, y @ControllerAdvice para manejar errores de forma centralizada.
Beneficio: validaciones limpias, automáticas, y respuestas uniformes ante errores de entrada.
🧰 4. Inversión de dependencias y testabilidad
- TaskService y TaskMapper son fácilmente testeables y mockeables.
- El controlador es delgado y desacoplado, ideal para pruebas con MockMVC o RestAssured.
Beneficio: código listo para escalar y con bajo acoplamiento, ideal para equipos grandes o proyectos profesionales.
⚖️ ¿Vale la pena todo este esfuerzo?
Sí, si tu proyecto:
- Tiene múltiples endpoints y va a escalar.
- Involucra equipos de desarrollo.
- Necesita testeo automatizado.
- Requiere exponer solo ciertos campos y proteger la lógica de negocio.
En un proyecto simple o prototipo, puedes reducir el overhead, pero en un entorno profesional, esta arquitectura es altamente recomendable.
Nota: Próximamente publicaré una serie de post donde profundizaré más sobre los temas abordados acá.
🌐 Recursos web oficiales para profundizar
- Spring Boot
- Guía Quickstart
- Construyendo una aplicación con Spring Boot
- Construyendo servicios web RESTful
- Guías
Historial de actualizaciones
- 17/06/2025: Añadida sección sobre primeros pasos para crear la aplicación.
- 16/06/2025: Añadida sección sobre implementación avanzada con DTOs, mappers, y validación. Ampliada la conclusión para destacar los beneficios de esta arquitectura.
- 10/06/2025: Publicación original.