TP3.1 - Arquitectura en Capas y API CRUD Completa
Aplicación de Gestión de Estudiantes
Objetivos
Este trabajo práctico le permitirá:
- Comprender la arquitectura en capas (Controller → Service → Repository)
- Crear una API REST CRUD completa
- Implementar las operaciones Create, Read, Update, Delete
- Utilizar Spring Data JPA con H2 (base en memoria)
- Probar una API REST con Postman o un navegador
- Gestionar errores y validaciones
Tabla de Contenidos
- Arquitectura en Capas
- Creación del Proyecto
- Creación de la Entidad Student
- Creación del Repository
- Creación del Service
- Creación del Controller
- Configuración H2
- Pruebas de los Endpoints
- Ejercicios Adicionales
Recursos
Documentación con capturas de pantalla detalladas:
📸 Referencias de TP3.1 - Google Drive
1. Arquitectura en Capas
Vista General
La arquitectura en capas separa las responsabilidades en 4 niveles distintos:
+----------------+ +----------------+ +----------------+ +----------------+
| | | | | | | |
| Controller +------>+ Service +------>+ Repository +------>+ Database |
| | | | | | | |
+----------------+ +----------------+ +----------------+ +----------------+
(HTTP) (Lógica de Negocio) (Acceso a Datos) (Almacenamiento)
Diagrama Mermaid
Responsabilidades de Cada Capa
1. Controller (Capa de Presentación)
Rol:
- Punto de entrada de la aplicación
- Recibe las solicitudes HTTP (GET, POST, PUT, DELETE)
- Analiza los parámetros de la solicitud
- Llama al Service apropiado
- Devuelve la respuesta al cliente (JSON)
Anotaciones:
@RestController@RequestMapping@GetMapping,@PostMapping,@PutMapping,@DeleteMapping
Ejemplo:
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping
public List<Student> getAllStudents() {
return studentService.getAllStudents();
}
}
2. Service (Capa de Negocio)
Rol:
- Contiene la lógica de negocio de la aplicación
- Intermediario entre Controller y Repository
- Aplica las reglas de validación
- Realiza cálculos y transformaciones
- Gestiona las transacciones
Anotaciones:
@Service@Transactional(para transacciones)
Ejemplo:
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}
3. Repository (Capa de Acceso a Datos)
Rol:
- Interactúa directamente con la base de datos
- Realiza las operaciones CRUD
- Utiliza Spring Data JPA
- Generación automática de consultas SQL
Anotaciones:
@Repository- Interfaz que extiende
JpaRepository
Ejemplo:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Métodos generados automáticamente por Spring Data JPA
}
4. Database (Capa de Persistencia)
Rol:
- Almacenamiento físico de los datos
- Gestión de transacciones ACID
- Indexación y optimización
Tecnologías:
- H2 (base en memoria para desarrollo/pruebas)
- PostgreSQL, MySQL (producción)
Flujo de una Solicitud
2. Creación del Proyecto
2.1 Spring Initializr
Vaya a https://start.spring.io/
Configuración:
| Parámetro | Valor |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.2.0 (o última versión estable) |
| Group | com.example |
| Artifact | student-management |
| Name | student-management |
| Package name | com.example.student |
| Packaging | Jar |
| Java | 17 |
Dependencias a agregar:
- Spring Web - Para crear la API REST
- Spring Data JPA - Para la persistencia
- H2 Database - Base de datos en memoria
- Validation - Para validar los datos
- Spring Boot DevTools - Para recarga automática
2.2 Importación en VS Code
- Descargue el proyecto (ZIP)
- Descomprímalo en su espacio de trabajo
- Abra VS Code
- File → Open Folder → Seleccione
student-management - Espere a que Maven descargue las dependencias
2.3 Estructura del Proyecto
Cree la siguiente estructura de paquetes:
src/main/java/com/example/student/
├── StudentManagementApplication.java
├── controller/
│ └── StudentController.java
├── service/
│ └── StudentService.java
├── repository/
│ └── StudentRepository.java
├── model/
│ └── Student.java
└── exception/
└── ResourceNotFoundException.java
3. Creación de la Entidad Student
3.1 Clase Student
Cree src/main/java/com/example/student/model/Student.java:
package com.example.student.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Le prénom est obligatoire")
@Size(min = 2, max = 50, message = "Le prénom doit contenir entre 2 et 50 caractères")
@Column(name = "first_name", nullable = false)
private String firstName;
@NotBlank(message = "Le nom est obligatoire")
@Size(min = 2, max = 50, message = "Le nom doit contenir entre 2 et 50 caractères")
@Column(name = "last_name", nullable = false)
private String lastName;
@NotBlank(message = "L'email est obligatoire")
@Email(message = "Format d'email invalide")
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "age")
private Integer age;
// Constructeur par défaut (requis par JPA)
public Student() {
}
// Constructeur avec paramètres
public Student(String firstName, String lastName, String email, Integer age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}
// Getters et Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", age=" + age +
'}';
}
}
3.2 Explicación de las Anotaciones
| Anotación | Rol |
|---|---|
@Entity | Marca la clase como entidad JPA |
@Table(name = "students") | Nombre de la tabla SQL |
@Id | Clave primaria |
@GeneratedValue(strategy = GenerationType.IDENTITY) | Auto-incremento |
@Column | Configuración de la columna |
@NotBlank | Validación: campo no vacío |
@Size | Validación: tamaño mínimo/máximo |
@Email | Validación: formato de email |
4. Creación del Repository
4.1 Interfaz StudentRepository
Cree src/main/java/com/example/student/repository/StudentRepository.java:
package com.example.student.repository;
import com.example.student.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
// Spring Data JPA génère automatiquement l'implémentation
// Méthodes personnalisées (optionnelles)
Optional<Student> findByEmail(String email);
boolean existsByEmail(String email);
}
4.2 Métodos Disponibles
Métodos heredados de JpaRepository:
| Método | Descripción |
|---|---|
save(Student s) | Guardar o actualizar |
findAll() | Recuperar todos los estudiantes |
findById(Long id) | Recuperar por ID |
deleteById(Long id) | Eliminar por ID |
count() | Contar los registros |
existsById(Long id) | Verificar existencia |
Métodos personalizados:
findByEmail(String email)- Buscar por emailexistsByEmail(String email)- Verificar si el email existe
Nota: ¡Spring Data JPA genera automáticamente las consultas SQL basadas en el nombre del método!
5. Creación del Service
5.1 Clase StudentService
Cree src/main/java/com/example/student/service/StudentService.java:
package com.example.student.service;
import com.example.student.model.Student;
import com.example.student.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
// CREATE - Crear un estudiante
public Student createStudent(Student student) {
// Verificar si el email ya existe
if (studentRepository.existsByEmail(student.getEmail())) {
throw new RuntimeException("Ya existe un estudiante con este email");
}
return studentRepository.save(student);
}
// READ - Recuperar todos los estudiantes
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
// READ - Recuperar un estudiante por ID
public Optional<Student> getStudentById(Long id) {
return studentRepository.findById(id);
}
// UPDATE - Actualizar un estudiante
public Student updateStudent(Long id, Student studentDetails) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Estudiante no encontrado con el id: " + id));
student.setFirstName(studentDetails.getFirstName());
student.setLastName(studentDetails.getLastName());
student.setEmail(studentDetails.getEmail());
student.setAge(studentDetails.getAge());
return studentRepository.save(student);
}
// DELETE - Eliminar un estudiante
public void deleteStudent(Long id) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Estudiante no encontrado con el id: " + id));
studentRepository.delete(student);
}
// Método utilitario - Buscar por email
public Optional<Student> getStudentByEmail(String email) {
return studentRepository.findByEmail(email);
}
}
5.2 Lógica de Negocio
El Service contiene:
- Validación: Verificar si el email existe antes de crear
- Gestión de errores: Lanzar excepciones si el estudiante no se encuentra
- Orquestación: Coordinar las llamadas al Repository
- Transformación: Mapear los datos si es necesario
6. Creación del Controller
6.1 Clase StudentController
Cree src/main/java/com/example/student/controller/StudentController.java:
package com.example.student.controller;
import com.example.student.model.Student;
import com.example.student.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
@Autowired
private StudentService studentService;
// CREATE - POST /api/students
@PostMapping
public ResponseEntity<Student> createStudent(@Valid @RequestBody Student student) {
try {
Student createdStudent = studentService.createStudent(student);
return new ResponseEntity<>(createdStudent, HttpStatus.CREATED);
} catch (RuntimeException e) {
return new ResponseEntity<>(null, HttpStatus.CONFLICT);
}
}
// READ ALL - GET /api/students
@GetMapping
public ResponseEntity<List<Student>> getAllStudents() {
List<Student> students = studentService.getAllStudents();
return new ResponseEntity<>(students, HttpStatus.OK);
}
// READ ONE - GET /api/students/{id}
@GetMapping("/{id}")
public ResponseEntity<Student> getStudentById(@PathVariable Long id) {
return studentService.getStudentById(id)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
// UPDATE - PUT /api/students/{id}
@PutMapping("/{id}")
public ResponseEntity<Student> updateStudent(
@PathVariable Long id,
@Valid @RequestBody Student studentDetails) {
try {
Student updatedStudent = studentService.updateStudent(id, studentDetails);
return new ResponseEntity<>(updatedStudent, HttpStatus.OK);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// DELETE - DELETE /api/students/{id}
@DeleteMapping("/{id}")
public ResponseEntity<HttpStatus> deleteStudent(@PathVariable Long id) {
try {
studentService.deleteStudent(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (RuntimeException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
// SEARCH BY EMAIL - GET /api/students/email/{email}
@GetMapping("/email/{email}")
public ResponseEntity<Student> getStudentByEmail(@PathVariable String email) {
return studentService.getStudentByEmail(email)
.map(student -> new ResponseEntity<>(student, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}
6.2 Endpoints REST
| Método HTTP | Endpoint | Acción | Código de Éxito |
|---|---|---|---|
| POST | /api/students | Crear un estudiante | 201 CREATED |
| GET | /api/students | Listar todos los estudiantes | 200 OK |
| GET | /api/students/\{id\} | Recuperar un estudiante | 200 OK |
| PUT | /api/students/\{id\} | Actualizar un estudiante | 200 OK |
| DELETE | /api/students/\{id\} | Eliminar un estudiante | 204 NO CONTENT |
| GET | /api/students/email/\{email\} | Buscar por email | 200 OK |
6.3 Códigos de Estado HTTP
7. Configuración H2
7.1 Archivo application.properties
Cree/modifique src/main/resources/application.properties:
# Port du serveur
server.port=8080
# Configuration H2 Database
spring.datasource.url=jdbc:h2:mem:studentdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# Configuration JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Console H2 (accessible via navigateur)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
7.2 Acceso a la Consola H2
Una vez iniciada la aplicación, acceda a:
http://localhost:8080/h2-console
Parámetros de conexión:
| Campo | Valor |
|---|---|
| JDBC URL | jdbc:h2:mem:studentdb |
| User Name | sa |
| Password | (dejar vacío) |
8. Pruebas de los Endpoints
8.1 Inicio de la Aplicación
En VS Code:
mvn clean install -DskipTests spring-boot:run
Logs esperados:
Hibernate: create table students (...)
Started StudentManagementApplication in X.XXX seconds
8.2 Pruebas con el Navegador
Prueba 1: Crear un estudiante (POST)
Nota: El navegador no puede realizar POST fácilmente. Utilice Postman o curl.
Con curl (PowerShell/CMD):
curl -X POST http://localhost:8080/api/students ^
-H "Content-Type: application/json" ^
-d "{\"firstName\":\"Jean\",\"lastName\":\"Dupont\",\"email\":\"jean.dupont@example.com\",\"age\":20}"
Prueba 2: Listar todos los estudiantes (GET)
En el navegador:
http://localhost:8080/api/students
Respuesta esperada:
[
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
]
Prueba 3: Recuperar un estudiante por ID (GET)
http://localhost:8080/api/students/1
Respuesta esperada:
{
"id": 1,
"firstName": "Jean",
"lastName": "Dupont",
"email": "jean.dupont@example.com",
"age": 20
}
8.3 Pruebas con Postman
Instalación de Postman
- Descargue Postman: https://www.postman.com/downloads/
- Instale y abra Postman
Colección de Pruebas
Prueba 1: CREATE (POST)
- Método: POST
- URL:
http://localhost:8080/api/students - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin",
"email": "alice.martin@example.com",
"age": 22
}
Resultado esperado: Status 201 CREATED
Prueba 2: READ ALL (GET)
- Método: GET
- URL:
http://localhost:8080/api/students
Resultado esperado: Status 200 OK + Lista de estudiantes
Prueba 3: READ ONE (GET)
- Método: GET
- URL:
http://localhost:8080/api/students/1
Resultado esperado: Status 200 OK + Detalles del estudiante
Prueba 4: UPDATE (PUT)
- Método: PUT
- URL:
http://localhost:8080/api/students/1 - Headers:
Content-Type: application/json - Body (raw JSON):
{
"firstName": "Alice",
"lastName": "Martin-Modifié",
"email": "alice.martin.updated@example.com",
"age": 23
}
Resultado esperado: Status 200 OK + Estudiante actualizado
Prueba 5: DELETE (DELETE)
- Método: DELETE
- URL:
http://localhost:8080/api/students/1
Resultado esperado: Status 204 NO CONTENT
Prueba 6: SEARCH BY EMAIL (GET)
- Método: GET
- URL:
http://localhost:8080/api/students/email/alice.martin@example.com
Resultado esperado: Status 200 OK + Estudiante encontrado
8.4 Verificación en la Consola H2
- Acceda a
http://localhost:8080/h2-console - Inicie sesión
- Ejecute la consulta:
SELECT * FROM students;
Debería ver todos los estudiantes creados.
9. Ejercicios Adicionales
Ejercicio 1: Agregar un Campo "Programa"
Objetivo: Agregar un campo program (String) a la entidad Student
Instrucciones:
- Modifique la clase
Student.java - Agregue el campo
program - Genere los getters/setters
- Pruebe la creación de un estudiante con un programa
Ejercicio 2: Búsqueda por Apellido
Objetivo: Implementar una búsqueda por apellido
Instrucciones:
- Agregue un método en
StudentRepository:List<Student> findByLastName(String lastName); - Agregue un método en
StudentService - Agregue un endpoint en
StudentController:@GetMapping("/lastname/\{lastName\}") - Pruebe con Postman
Ejercicio 3: Paginación
Objetivo: Agregar paginación a la lista de estudiantes
Instrucciones:
- Modifique el método
getAllStudents()en el Service:public Page<Student> getAllStudents(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return studentRepository.findAll(pageable);
} - Modifique el Controller para aceptar los parámetros
pageysize - Pruebe:
http://localhost:8080/api/students?page=0&size=5
Ejercicio 4: Gestión de Errores Personalizada
Objetivo: Crear una gestión global de errores
Instrucciones:
- Cree
ResourceNotFoundException.java:
package com.example.student.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
- Cree
GlobalExceptionHandler.java:
package com.example.student.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
- Utilice
ResourceNotFoundExceptionen el Service - Pruebe intentando acceder a un estudiante inexistente
Ejercicio 5: Validación Avanzada
Objetivo: Agregar validaciones personalizadas
Instrucciones:
- Agregue restricciones en
Student.java:@Min(value = 18, message = "L'âge minimum est 18 ans")
@Max(value = 100, message = "L'âge maximum est 100 ans")
private Integer age; - Pruebe creando un estudiante con una edad inválida
- Observe el código 400 BAD REQUEST
Ejercicio 6: Migración a PostgreSQL
Objetivo: Reemplazar H2 por PostgreSQL
Instrucciones:
- Agregue la dependencia PostgreSQL en
pom.xml - Modifique
application.properties:spring.datasource.url=jdbc:postgresql://localhost:5432/studentdb
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update - Cree la base de datos
studentdben PostgreSQL - Reinicie la aplicación
- Verifique que los datos persisten después de reiniciar
Resolución de Problemas
Problema 1: Error 404 en todos los endpoints
Causa: El Controller no es escaneado por Spring
Solución:
Verifique que StudentController esté en el mismo paquete (o subpaquete) que StudentManagementApplication.
Problema 2: Error de validación
Síntoma:
Validation failed for object='student'
Solución:
Verifique que los datos enviados respeten las restricciones @NotBlank, @Email, @Size, etc.
Problema 3: Consola H2 no accesible
Síntoma:
404 on /h2-console
Solución:
Verifique en application.properties:
spring.h2.console.enabled=true
Problema 4: Conflicto de email
Síntoma:
Status 409 CONFLICT
Solución:
¡Normal! Ya existe un estudiante con ese email. Es la validación funcionando.
Preguntas de Repaso
-
¿Cuál es la responsabilidad de cada capa (Controller, Service, Repository)?
-
¿Por qué separar el código en capas?
-
¿Cuál es la diferencia entre
@RestControllery@Service? -
¿Qué hace la anotación
@Validen el Controller? -
¿Cómo genera Spring Data JPA las consultas SQL?
-
¿Cuál es la diferencia entre
save()para crear y para actualizar? -
¿Por qué usar
Optional<Student>en el Service? -
¿Qué código HTTP devolver después de una eliminación exitosa?
Entregables
A entregar:
- Código fuente completo del proyecto
- Capturas de pantalla de Postman mostrando las 6 pruebas exitosas
- Captura de pantalla de la Consola H2 con datos
- Al menos 2 ejercicios completados
- Documento de respuestas a las preguntas de repaso
Criterios de Evaluación
| Criterio | Puntos |
|---|---|
| Arquitectura en capas correctamente implementada | 20% |
| Entidad Student con validaciones | 15% |
| Repository funcional | 10% |
| Service con lógica de negocio | 20% |
| Controller con todos los endpoints CRUD | 20% |
| Pruebas exitosas (Postman/Navegador) | 10% |
| Ejercicios adicionales | 5% |
| Total | 100% |
Resumen
Lo que ha aprendido
- Arquitectura en capas (Controller → Service → Repository → Database)
- Creación de una API REST CRUD completa
- Operaciones CRUD: Create, Read, Update, Delete
- Validación de datos con Bean Validation
- Gestión de códigos de estado HTTP
- Uso de H2 para desarrollo
- Pruebas de API con Postman
Buenas Prácticas
- Separar las responsabilidades en capas distintas
- Validar los datos en la entrada (Controller)
- Gestionar los errores adecuadamente (excepciones personalizadas)
- Utilizar los códigos HTTP apropiados
- Probar todos los endpoints
- Documentar la API
Próximos Pasos
En los próximos TPs, veremos:
- TP4: Seguridad con Spring Security
- TP5: Documentación con Swagger/OpenAPI
- TP6: Despliegue en producción