Un proyecto educativo diseñado para estudiantes de segundo año de Ingeniería en Computación que enseña los principios fundamentales de Spring Framework, decoradores básicos y de validación, buenas prácticas de APIs REST con HTTP status codes, y manejo centralizado de errores.
Este ejercicio corresponde al ramo Fullstack I para estudiantes de Ingeniería en Informática de DUOC UC.
- Comprender la arquitectura de una aplicación Spring Boot
- Dominar decoradores (anotaciones) básicas y de validación
- Implementar APIs REST siguiendo buenas prácticas
- Usar códigos de estado HTTP apropiados
- Implementar manejo centralizado de excepciones
- Aplicar inyección de dependencias
- Utilizar el patrón de diseño CSR (Controller-Service-Repository): adaptación de MVC para Spring Boot
- Java 21 o superior
- Maven 3.6+ para gestión de dependencias
- IDE recomendado: IntelliJ IDEA, Visual Studio Code o Eclipse
- Conocimientos básicos de Java y POO
- Conceptos básicos de APIs REST y HTTP
<!-- Spring Boot Starter Web (REST Controllers y servidores web) -->
spring-boot-starter-web
<!-- Spring Boot Starter Validation (Validación de datos) -->
spring-boot-starter-validation
<!-- Lombok (Generación automática de getters, setters, etc.) -->
lombok
<!-- Spring Boot Starter Test (Pruebas unitarias) -->
spring-boot-starter-testclientes/
├── src/
│ ├── main/
│ │ ├── java/com/duoc/clientes/
│ │ │ ├── ClientesApplication.java # Punto de entrada
│ │ │ ├── controller/
│ │ │ │ └── ClientesController.java # Endpoints REST
│ │ │ ├── model/
│ │ │ │ └── ClientesModel.java # Entidad con validaciones
│ │ │ ├── service/
│ │ │ │ └── ClientesService.java # Lógica de negocio
│ │ │ ├── repository/
│ │ │ │ └── ClientesRepository.java # Acceso a datos
│ │ │ └── exception/
│ │ │ └── GlobalExceptionHandler.java # Manejo de errores
│ │ └── resources/
│ │ └── application.properties # Configuración
│ └── test/
│ └── ClientesApplicationTests.java # Pruebas
└── pom.xml # Configuración Maven
Las anotaciones son etiquetas especiales que proporcionan metadatos sobre el programa, no affecting directly the operation of the code but providing information to the framework.
Ubicación: ClientesApplication.java
@SpringBootApplication
public class ClientesApplication {
public static void main(String[] args) {
SpringApplication.run(ClientesApplication.class, args);
}
}¿Qué hace? Combina tres anotaciones en una:
@Configuration: Marca la clase como fuente de definiciones de beans@EnableAutoConfiguration: Permite que Spring Boot configure automáticamente la aplicación basándose en las dependencias@ComponentScan: Escanea el paquete actual y subpaquetes buscando componentes anotados
Ubicación: ClientesController.java
@RestController
@RequestMapping("/api/v1/clientes")
public class ClientesController {
// ...
}¿Qué hace?
- Marca la clase como controlador REST
- Equivalente a
@Controller+@ResponseBody - Los métodos retornan datos serializados (JSON) automáticamente
@RequestMapping("/api/v1/clientes"): Define la ruta base para todos los endpoints
Ubicación: ClientesController.java
@Autowired
private ClientesService clientesService;¿Qué hace?
- Inyección de Dependencias: Spring inyecta automáticamente una instancia de
ClientesService - No necesitas crear la instancia manualmente con
new - Spring gestiona el ciclo de vida del objeto
- Promueve el desacoplamiento y facilita las pruebas
Ubicación: ClientesService.java
@Service
public class ClientesService {
// ...
}¿Qué hace?
- Marca la clase como un bean de servicio (componente del negocio)
- Contiene la lógica empresarial
- Spring la detecta automáticamente y la registra como bean
- Se puede inyectar en otros componentes
Ubicación: ClientesRepository.java
@Repository
public class ClientesRepository {
// ...
}¿Qué hace?
- Marca la clase como un repositorio (capa de acceso a datos)
- Responsable de CRUD (Create, Read, Update, Delete)
- En este proyecto, usa un ArrayList en memoria (simulación)
- En proyectos reales, se conectaría a una base de datos
Garantizan que los datos recibidos cumplan con reglas específicas antes de procesarlos.
Ubicación: ClientesModel.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClientesModel {
@NotNull(message = "El nombre no puede ser null")
@NotBlank(message = "El nombre no puede estar vacio")
private String nombre;
@NotNull(message = "El correo no puede ser null")
@NotBlank(message = "El correo no puede estar vacio")
private String correo;
@NotNull(message = "La edad no puede ser null")
@Positive(message = "La edad debe ser mayor a cero")
private Integer edad;
}- Valida: El valor no puede ser
null - Tipo: Cualquier tipo de datos
- Diferencia con @NotBlank: Permite strings vacíos
""
- Valida: El string no puede ser
nullni estar vacío - Trim: Considera espacios en blanco como vacío
- Tipo: Solo strings
- Valida: El número debe ser mayor que 0
- Tipo: Números (Integer, Double, BigDecimal, etc.)
@Data // Genera: getters, setters, equals(), hashCode(), toString()
@NoArgsConstructor // Genera constructor sin argumentos
@AllArgsConstructor // Genera constructor con todos los campos@RestController
@RequestMapping("/api/v1/clientes")
public class ClientesController {
// GET - Obtener todos los clientes
@GetMapping
public ResponseEntity<List<ClientesModel>> listarClientes() { }
// POST - Crear nuevo cliente
@PostMapping
public ResponseEntity<ClientesModel> agregarCliente(
@Valid @RequestBody ClientesModel cliente
) { }
// DELETE - Eliminar cliente por correo
@DeleteMapping("{correo}")
public ResponseEntity<String> eliminarCliente(
@PathVariable String correo
) { }
}| Decorator | Verbo HTTP | Operación | Descripción |
|---|---|---|---|
@GetMapping |
GET | Read | Obtiene datos sin modificarlos |
@PostMapping |
POST | Create | Crea un nuevo recurso |
@PutMapping |
PUT | Update | Actualiza completamente un recurso |
@PatchMapping |
PATCH | Partial Update | Actualiza parcialmente un recurso |
@DeleteMapping |
DELETE | Delete | Elimina un recurso |
// @RequestBody: Mapea el JSON del request al objeto
@PostMapping
public ResponseEntity<ClientesModel> agregarCliente(
@RequestBody ClientesModel cliente
) { }
// @Valid: Activa la validación de datos
@PostMapping
public ResponseEntity<ClientesModel> agregarCliente(
@Valid @RequestBody ClientesModel cliente
) { }
// @PathVariable: Extrae valores de la ruta
@DeleteMapping("{correo}")
public ResponseEntity<String> eliminarCliente(
@PathVariable String correo
) { }Los códigos HTTP comunican al cliente el resultado de su request.
| Código | Nombre | Significado | Cuándo Usar |
|---|---|---|---|
| 200 | OK | La solicitud fue exitosa | GET exitoso, DELETE exitoso |
| 201 | Created | Recurso creado exitosamente | POST que crea un recurso |
| 400 | Bad Request | Solicitud inválida o error en validación | Datos inválidos, errores de negocio |
| 404 | Not Found | Recurso no encontrado | GET de recurso inexistente |
| 500 | Internal Server Error | Error del servidor | Excepciones no controladas |
// GET exitoso (200)
@GetMapping
public ResponseEntity<List<ClientesModel>> listarClientes() {
return ResponseEntity.status(200).body(clientesService.getClientes());
}
// POST exitoso (201)
@PostMapping
public ResponseEntity<ClientesModel> agregarCliente(@Valid @RequestBody ClientesModel cliente) {
return ResponseEntity.status(201).body(clientesService.saveCliente(cliente));
}
// Error en validación (400)
// Manejado automáticamente por GlobalExceptionHandlerEn lugar de manejar errores en cada endpoint, Spring permite centralizar la gestión en una clase especial.
Ubicación: GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex
) {
Map<String, String> errores = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errores.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errores);
}
}¿Cómo funciona?
@RestControllerAdvice: Marca la clase como handler global de excepciones@ExceptionHandler: Define qué tipo de excepción maneja este método- Cuando alguien envía un cliente sin nombre, Spring:
- Lanza
MethodArgumentNotValidException GlobalExceptionHandlerlo captura- Retorna un JSON con los errores específicos
- Lanza
Respuesta de Error (ejemplo):
{
"nombre": "El nombre no puede estar vacio",
"edad": "La edad debe ser mayor a cero"
}Si deseas crear un proyecto similar desde cero, puedes usar Spring Initializr:
- Accede a: start.spring.io
- Configura:
- Project: Maven Project
- Language: Java
- Spring Boot: 4.0.4 o superior
- Group: com.duoc
- Artifact: clientes
- Java Version: 21
- Dependencias (Add Dependencies):
- Spring Web
- Spring Boot Validation
- Lombok
- Click "Generate"
- Descomprime y abre en tu IDE favorito
curl https://start.spring.io/starter.zip \
-d dependencies=web,validation,lombok \
-d javaVersion=21 \
-d bootVersion=4.0.4 \
-d groupId=com.duoc \
-d artifactId=clientes \
-o clientes.zip
unzip clientes.zip
cd clientescd tu-directorio
# Si es un repositorio git:
git clone <repositorio>
cd clientesjava -version
# Debe mostrar Java 21 o superiormvn clean compileclean: Elimina compilaciones anteriorescompile: Compila el código fuente
mvn spring-boot:runSalida esperada:
[INFO] Started ClientesApplication in 2.5 seconds
La aplicación estará disponible en: http://localhost:8080
- Postman: Interfaz gráfica completa
- cURL: Línea de comandos (incluido en macOS/Linux)
- Thunder Client: Extensión de VS Code
- Insomnia: Similar a Postman
curl -X GET http://localhost:8080/api/v1/clientesRespuesta esperada (200):
[]curl -X POST http://localhost:8080/api/v1/clientes \
-H "Content-Type: application/json" \
-d '{
"nombre": "Juan Pérez",
"correo": "juan@example.com",
"edad": 25
}'Respuesta esperada (201):
{
"nombre": "Juan Pérez",
"correo": "juan@example.com",
"edad": 25
}curl -X POST http://localhost:8080/api/v1/clientes \
-H "Content-Type: application/json" \
-d '{
"nombre": "",
"correo": "juan@example.com",
"edad": -5
}'Respuesta esperada (400):
{
"nombre": "El nombre no puede estar vacio",
"edad": "La edad debe ser mayor a cero"
}curl -X GET http://localhost:8080/api/v1/clientesRespuesta esperada (200):
[
{
"nombre": "Juan Pérez",
"correo": "juan@example.com",
"edad": 25
}
]curl -X DELETE http://localhost:8080/api/v1/clientes/juan@example.comRespuesta esperada (200):
"Cliente eliminado"┌─────────────────────────────────────────────────────────────────┐
│ CLIENTE (Postman, cURL) │
└────────────────────────┬──────────────────────────────────────────┘
│ 1. Envía JSON
▼
┌─────────────────────────────────────────────────────────────────┐
│ ClientesController │
│ - Recibe request HTTP │
│ - Mapea JSON → ClientesModel (@RequestBody) │
│ - Valida datos (@Valid) │
│ - Si hay errores → GlobalExceptionHandler (captura) │
└────────────────────────┬──────────────────────────────────────────┘
│ 2. Si válido, llama servicio
▼
┌─────────────────────────────────────────────────────────────────┐
│ ClientesService │
│ - Contiene lógica de negocio │
│ - Procesa los datos │
│ - Llama al repositorio │
└────────────────────────┬──────────────────────────────────────────┘
│ 3. Persistencia
▼
┌─────────────────────────────────────────────────────────────────┐
│ ClientesRepository │
│ - ArrayList (simula base de datos en memoria) │
│ - CRUD: Crear, Leer, Actualizar, Eliminar │
│ - Retorna datos procesados │
└────────────────────────┬──────────────────────────────────────────┘
│ 4. Respuesta
▼
┌─────────────────────────────────────────────────────────────────┐
│ ClientesController │
│ - ResponseEntity with HTTP Status (200, 201, 400) │
│ - Serializa objeto → JSON │
└────────────────────────┬──────────────────────────────────────────┘
│ 5. Envía JSON
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLIENTE (Recibe respuesta) │
└─────────────────────────────────────────────────────────────────┘
CSR es la versión adaptada del patrón MVC específicamente para Spring Boot y APIs REST.
| Capa | Componente | Archivo | Responsabilidad |
|---|---|---|---|
| Presentación | Controller | ClientesController.java |
Mapea rutas HTTP, recibe requests, retorna responses |
| Lógica de Negocio | Service | ClientesService.java |
Contiene la lógica de negocio, orquesta operaciones |
| Datos | Repository | ClientesRepository.java |
Acceso a datos, persistencia, operaciones CRUD |
| Datos | Model | ClientesModel.java |
Representa la entidad con atributos y validaciones |
En aplicaciones REST con Spring Boot:
- No hay "View" tradicional (HTML) → La respuesta es JSON
- La separación de responsabilidades es más clara
- Service maneja la lógica de negocio (no el Controller)
- Repository aísla el acceso a datos
- Facilita testing y mantenimiento
✅ Código más limpio y organizado
✅ Fácil de testear (inyección de dependencias)
✅ Escalable (añadir funcionalidades sin afectar otras capas)
✅ Reutilizable (Service puede usarse desde múltiples Controllers)
✅ Mantenible (cambios aislados por capa)
Cada clase tiene una único propósito claro:
- Controller: Mapeo HTTP
- Service: Lógica de negocio
- Repository: Acceso a datos
@Autowired
private ClientesService clientesService;Facilita: Testing, desacoplamiento, mantenibilidad
@PostMapping
public ResponseEntity<ClientesModel> agregarCliente(@Valid @RequestBody ClientesModel cliente)Previene procesamiento de datos inválidos
- 200: Operación exitosa
- 201: Recurso creado
- 400: Solicitud inválida
- 500: Error del servidor
En lugar de try-catch en cada método, usar @RestControllerAdvice
/api/v1/clientes permite evitar conflictos al cambiar la API
- Agregar un nuevo campo
telefonoaClientesModelcon validación - Crear un endpoint PUT para actualizar cliente
- Agregar validación de email usando
@Email
- Agregar manejo de excepción personalizada (ej: ClienteNoEncontradoException)
- Implementar búsqueda de cliente por correo (GET con parámetro)
- Agregar información de error más detallada en respuestas
- Documentación oficial de Spring Boot
- Spring Framework Reference
- Jakarta Bean Validation
- RESTful Web Services (HTTP Status Codes)
# Cambiar puerto en application.properties:
server.port=8081Este error significa que la inyección de dependencias no encontró el bean. Asegurate:
- La clase
ClientesServicetiene@Service - El controlador tiene
@Autowired - Spring escanea el paquete correcto
El JSON enviado tiene formato inválido. Verifica:
- Comillas dobles en propiedades
- Tipos de datos correctos
- No hay caracteres de escape faltantes
Este proyecto es de código abierto y está disponible bajo la licencia MIT, diseñado para propósitos educativos.
Última actualización: 28 de marzo de 2026