Saltar al contenido principal

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​

  1. Arquitectura en Capas
  2. Creación del Proyecto
  3. Creación de la Entidad Student
  4. Creación del Repository
  5. Creación del Service
  6. Creación del Controller
  7. Configuración H2
  8. Pruebas de los Endpoints
  9. 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ámetroValor
ProjectMaven
LanguageJava
Spring Boot3.2.0 (o última versión estable)
Groupcom.example
Artifactstudent-management
Namestudent-management
Package namecom.example.student
PackagingJar
Java17

Dependencias a agregar:

  1. Spring Web - Para crear la API REST
  2. Spring Data JPA - Para la persistencia
  3. H2 Database - Base de datos en memoria
  4. Validation - Para validar los datos
  5. Spring Boot DevTools - Para recarga automática

2.2 Importación en VS Code​

  1. Descargue el proyecto (ZIP)
  2. Descomprímalo en su espacio de trabajo
  3. Abra VS Code
  4. File → Open Folder → Seleccione student-management
  5. 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ónRol
@EntityMarca la clase como entidad JPA
@Table(name = "students")Nombre de la tabla SQL
@IdClave primaria
@GeneratedValue(strategy = GenerationType.IDENTITY)Auto-incremento
@ColumnConfiguración de la columna
@NotBlankValidación: campo no vacío
@SizeValidación: tamaño mínimo/máximo
@EmailValidació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étodoDescripció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 email
  • existsByEmail(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:

  1. Validación: Verificar si el email existe antes de crear
  2. Gestión de errores: Lanzar excepciones si el estudiante no se encuentra
  3. Orquestación: Coordinar las llamadas al Repository
  4. 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 HTTPEndpointAcciónCódigo de Éxito
POST/api/studentsCrear un estudiante201 CREATED
GET/api/studentsListar todos los estudiantes200 OK
GET/api/students/\{id\}Recuperar un estudiante200 OK
PUT/api/students/\{id\}Actualizar un estudiante200 OK
DELETE/api/students/\{id\}Eliminar un estudiante204 NO CONTENT
GET/api/students/email/\{email\}Buscar por email200 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:

CampoValor
JDBC URLjdbc:h2:mem:studentdb
User Namesa
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​

  1. Descargue Postman: https://www.postman.com/downloads/
  2. 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​

  1. Acceda a http://localhost:8080/h2-console
  2. Inicie sesión
  3. 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:

  1. Modifique la clase Student.java
  2. Agregue el campo program
  3. Genere los getters/setters
  4. Pruebe la creación de un estudiante con un programa

Ejercicio 2: Búsqueda por Apellido​

Objetivo: Implementar una búsqueda por apellido

Instrucciones:

  1. Agregue un método en StudentRepository:
    List<Student> findByLastName(String lastName);
  2. Agregue un método en StudentService
  3. Agregue un endpoint en StudentController:
    @GetMapping("/lastname/\{lastName\}")
  4. Pruebe con Postman

Ejercicio 3: Paginación​

Objetivo: Agregar paginación a la lista de estudiantes

Instrucciones:

  1. 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);
    }
  2. Modifique el Controller para aceptar los parámetros page y size
  3. 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:

  1. Cree ResourceNotFoundException.java:
package com.example.student.exception;

public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
  1. 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);
}
}
  1. Utilice ResourceNotFoundException en el Service
  2. Pruebe intentando acceder a un estudiante inexistente

Ejercicio 5: Validación Avanzada​

Objetivo: Agregar validaciones personalizadas

Instrucciones:

  1. 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;
  2. Pruebe creando un estudiante con una edad inválida
  3. Observe el código 400 BAD REQUEST

Ejercicio 6: Migración a PostgreSQL​

Objetivo: Reemplazar H2 por PostgreSQL

Instrucciones:

  1. Agregue la dependencia PostgreSQL en pom.xml
  2. 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
  3. Cree la base de datos studentdb en PostgreSQL
  4. Reinicie la aplicación
  5. 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​

  1. ¿Cuál es la responsabilidad de cada capa (Controller, Service, Repository)?

  2. ¿Por qué separar el código en capas?

  3. ¿Cuál es la diferencia entre @RestController y @Service?

  4. ¿Qué hace la anotación @Valid en el Controller?

  5. ¿Cómo genera Spring Data JPA las consultas SQL?

  6. ¿Cuál es la diferencia entre save() para crear y para actualizar?

  7. ¿Por qué usar Optional<Student> en el Service?

  8. ¿Qué código HTTP devolver después de una eliminación exitosa?


Entregables​

A entregar:

  1. Código fuente completo del proyecto
  2. Capturas de pantalla de Postman mostrando las 6 pruebas exitosas
  3. Captura de pantalla de la Consola H2 con datos
  4. Al menos 2 ejercicios completados
  5. Documento de respuestas a las preguntas de repaso

Criterios de Evaluación​

CriterioPuntos
Arquitectura en capas correctamente implementada20%
Entidad Student con validaciones15%
Repository funcional10%
Service con lógica de negocio20%
Controller con todos los endpoints CRUD20%
Pruebas exitosas (Postman/Navegador)10%
Ejercicios adicionales5%
Total100%

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

Recursos Adicionales​

Documentación​

Tutoriales​


Volver al Módulo 3