1.Rest Crud Basico
Primer contacto con Spring Boot y el desarrollo de APIs REST.
En este ejercicio se implementa un controlador básico para practicar los principales mecanismos de Spring Boot: creación de endpoints, recepción de parámetros mediante URL, uso de variables de ruta y procesamiento de objetos JSON enviados en el cuerpo de la petición.
La aplicación incluye varios ejemplos prácticos como saludos personalizados, una calculadora sencilla, validación de credenciales y recepción de datos de usuarios, permitiendo familiarizarse con las anotaciones más utilizadas en el desarrollo de servicios REST.
Conceptos trabajados:
* @RestController
* @GetMapping
* @PostMapping
* @PathVariable
* @RequestParam
* @RequestBody
* Creación de modelos Java
* Manejo básico de peticiones HTTP
* Validación de datos de entrada
package es.pulsoft.spring_basico.controller;
import es.pulsoft.spring_basico.model.Usuario;
import org.springframework.web.bind.annotation.*;
/**
* Controller básico para practicar conceptos fundamentales de Spring Boot REST.
* Incluye ejemplos de:
* - @GetMapping con PathVariable
* - @RequestParam para parámetros de consulta
* - @RequestBody para recibir JSON
*/
@RestController
public class BasicoController {
/**
* Endpoint simple de saludo.
* GET /saludo
*/
@GetMapping("/saludo")
public String saludo() {
return "Hola, bienvenido a la API";
}
/**
* Endpoint con PathVariable para saludo personalizado.
* Ejemplo: /saludo/Pul
*
* @param nombre nombre recibido en la URL
* @return mensaje personalizado
*/
@GetMapping("/saludo/{nombre}")
public String saludoPersonalizado(@PathVariable String nombre) {
return "Hola " + nombre;
}
/**
* Endpoint de calculadora básica.
* Permite suma o resta según el parámetro "op".
*
* Ejemplo:
* /calcular?op=suma&num1=5&num2=10
*
* @param op operación (suma o resta)
* @param num1 primer número
* @param num2 segundo número
* @return resultado de la operación
*/
@GetMapping("/calcular")
public int calcularSuma(@RequestParam String op,
@RequestParam int num1,
@RequestParam int num2) {
if (op.equalsIgnoreCase("suma")) {
return num1 + num2;
} else if (op.equalsIgnoreCase("resta")) {
return num1 - num2;
} else {
throw new IllegalArgumentException("Operación no válida");
}
}
/**
* Endpoint que recibe un usuario en formato JSON.
*
* Ejemplo de body:
* {
* "nombre": "Pul",
* "edad": 20
* }
*
* @param usuario objeto recibido desde el body
* @return mensaje confirmando recepción
*/
@PostMapping("/usuario")
public String datosUsuario(@RequestBody Usuario usuario) {
return "Usuario recibido: " + usuario.getNombre()
+ " tiene " + usuario.getEdad() + " años";
}
/**
* Endpoint de login simple.
* Simula autenticación básica con usuario y contraseña fijos.
*
* Ejemplo:
* /login?user=admin&pass=1234
*
* @param user usuario
* @param pass contraseña
* @return resultado del login
*/
@GetMapping("/login")
public String validarLogin(@RequestParam String user,
@RequestParam String pass) {
if (user.equals("admin") && pass.equals("1234")) {
return "Login correcto";
} else {
return "Login incorrecto";
}
}
}
package es.pulsoft.spring_basico.model;
public class Usuario {
private String nombre;
private int edad;
public Usuario() {
}
public Usuario(String nombre,int edad) {
this.nombre = nombre;
this.edad=edad;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public int getEdad() {
return edad;
}
public void setEdad(int edad) {
this.edad = edad;
}
}
API de Películas con Validaciones
Descripción
Desarrollo de una API REST para la gestión de películas utilizando Spring Boot y almacenamiento temporal en memoria mediante una colección ArrayList.
Conceptos trabajados
- Creación de controladores REST.
- Uso de @GetMapping, @PostMapping y @DeleteMapping.
- Recepción de parámetros mediante URL y JSON.
- Validaciones manuales de datos.
- Gestión de errores con ResponseStatusException.
- Uso de ResponseEntity.
- Búsquedas y filtrados mediante Streams.
Entidad principal
La aplicación gestiona películas con la siguiente información:
- Identificador.
- Título.
- Director.
- Duración.
- Estado de estreno.
Funcionalidades implementadas
- Listado completo de películas.
- Consulta de una película por identificador.
- Búsqueda de películas por director.
- Alta de nuevas películas mediante peticiones JSON.
- Eliminación de películas existentes.
Validaciones aplicadas
- El título es obligatorio.
- El director es obligatorio.
- La duración debe ser mayor que cero.
- No se permiten películas con títulos duplicados.
Gestión de errores
La aplicación devuelve respuestas HTTP adecuadas utilizando ResponseStatusException para gestionar errores de validación y recursos inexistentes.
package es.pulsoft.spring_peliculas.controller;
import es.pulsoft.spring_peliculas.model.Pelicula;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
/**
* Controlador REST para gestionar películas en memoria.
*
* Conceptos practicados:
* - GET, POST y DELETE
* - PathVariable
* - RequestParam
* - RequestBody
* - ResponseEntity
* - ResponseStatusException
* - Streams
* - Validaciones básicas
*/
@RestController
public class PeliculaController {
/** Contador para generar IDs automáticos. */
private int contadorIds = 1;
/** Lista que simula una base de datos en memoria. */
List<Pelicula> listaPeliculas = new ArrayList<>();
/**
* Obtiene todas las películas almacenadas.
*
* GET /peliculas
*
* @return lista completa de películas
*/
@GetMapping("/peliculas")
public List<Pelicula> listaPeliculas() {
return listaPeliculas;
}
/**
* Busca una película por su ID.
*
* GET /peliculas/{id}
*
* @param id identificador de la película
* @return película encontrada
*/
@GetMapping("/peliculas/{id}")
public Pelicula buscarId(@PathVariable int id) {
return buscarPorId(id);
}
/**
* Método auxiliar para localizar una película por ID.
*
* Si no existe, lanza una excepción 404.
*
* @param id identificador de la película
* @return película encontrada
*/
private Pelicula buscarPorId(int id) {
return listaPeliculas.stream()
.filter(n -> n.getId() == id)
.findFirst()
.orElseThrow(() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Pelicula no encontrada"));
}
/**
* Filtra películas por director.
*
* Ejemplo:
* GET /peliculas/buscar?director=Nolan
*
* @param director nombre del director
* @return lista de películas del director indicado
*/
@GetMapping("/peliculas/buscar")
public List<Pelicula> listarDirector(@RequestParam String director) {
return listaPeliculas.stream()
.filter(n -> n.getDirector().equalsIgnoreCase(director))
.toList();
}
/**
* Añade una nueva película a la lista.
*
* - Valida los datos recibidos.
* - Asigna un ID automático.
* - Devuelve 201 CREATED si todo es correcto.
*
* @param pelicula película recibida en formato JSON
* @return película creada
*/
@PostMapping("/peliculas")
public ResponseEntity<Pelicula> agregarPelicula(@RequestBody Pelicula pelicula) {
if (validaciones(pelicula)) {
pelicula.setId(contadorIds++);
listaPeliculas.add(pelicula);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(pelicula);
}
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Error al agregar la pelicula");
}
/**
* Realiza validaciones básicas:
* - Título obligatorio.
* - Director obligatorio.
* - Duración mayor que cero.
* - No permite títulos duplicados.
*
* @param pelicula película a validar
* @return true si es válida, false en caso contrario
*/
private boolean validaciones(Pelicula pelicula) {
if (pelicula.getTitulo() != null
&& !pelicula.getTitulo().isBlank()
&& pelicula.getDirector() != null
&& !pelicula.getDirector().isBlank()
&& pelicula.getDuracion() > 0) {
for (Pelicula p : listaPeliculas) {
if (p.getTitulo()
.equalsIgnoreCase(pelicula.getTitulo())) {
return false;
}
}
return true;
}
return false;
}
/**
* Elimina una película por ID.
*
* GET /peliculas/{id}
*
* @param id identificador de la película
* @return película eliminada
*/
@DeleteMapping("/peliculas/{id}")
public ResponseEntity<Pelicula> borrarPelicula(@PathVariable int id) {
Pelicula p = buscarPorId(id);
listaPeliculas.remove(p);
return ResponseEntity
.status(HttpStatus.OK)
.body(p);
}
}
package es.pulsoft.spring_peliculas.model;
public class Pelicula {
private int id;
private String titulo;
private String director;
private int duracion;
private boolean estrenada;
public Pelicula() {
}
public Pelicula(int id, String titulo, String director, int duracion, boolean estrenada) {
this.id = id;
this.titulo = titulo;
this.director = director;
this.duracion = duracion;
this.estrenada = estrenada;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getDirector() {
return director;
}
public void setDirector(String director) {
this.director = director;
}
public int getDuracion() {
return duracion;
}
public void setDuracion(int duracion) {
this.duracion = duracion;
}
public boolean isEstrenada() {
return estrenada;
}
public void setEstrenada(boolean estrenada) {
this.estrenada = estrenada;
}
}
API de Libros (CRUD Completo)
Descripción
Desarrollo de una API REST para la gestión de libros utilizando Spring Boot, aplicando operaciones CRUD completas y almacenamiento en memoria mediante una colección ArrayList.
Conceptos trabajados
- Creación de controladores REST.
- Uso de GET, POST, PUT, PATCH y DELETE.
- Manejo de ResponseEntity para respuestas HTTP.
- Gestión de errores con ResponseStatusException.
- Validaciones manuales de datos.
- Uso de Streams para búsquedas y filtrados.
- Generación de IDs automáticos.
- Reutilización de métodos internos.
Entidad principal
La aplicación gestiona libros con la siguiente estructura:
- Identificador.
- Título.
- Autor.
- Número de páginas.
- Estado de disponibilidad.
Almacenamiento
Se utiliza una lista en memoria:
- List<Libro> listaLibros = new ArrayList<>();
- Control de IDs mediante contador automático.
Funcionalidades implementadas
- Listado completo de libros.
- Consulta de libro por ID.
- Búsqueda de libros por autor.
- Creación de nuevos libros.
- Actualización completa de registros.
- Actualización parcial de disponibilidad.
- Eliminación de libros.
Endpoints
- GET /libros → obtener todos los libros.
- GET /libros/{id} → obtener libro por ID.
- GET /libros/buscar?autor= → búsqueda por autor.
- POST /libros → crear libro.
- PUT /libros/{id} → actualización completa.
- PATCH /libros/{id} → actualización parcial.
- DELETE /libros/{id} → eliminar libro.
Validaciones aplicadas
- El título es obligatorio.
- El autor es obligatorio.
- Las páginas deben ser mayores que 0.
- No se permiten títulos duplicados.
Gestión de errores
Se utilizan ResponseStatusException para controlar errores de validación y recursos no encontrados, devolviendo códigos HTTP adecuados.
Extras
Las respuestas de creación, actualización y eliminación se devuelven mediante ResponseEntity para un mejor control de los estados HTTP.
package es.pulsoft.spring_libros.controller;
import es.pulsoft.spring_libros.model.Libro;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
/**
* Controlador REST para la gestión de libros.
*
* Este controlador simula una API en memoria sin base de datos,
* incluyendo operaciones CRUD básicas y filtros.
*
* Conceptos aplicados:
* - REST API con Spring Boot
* - Operaciones CRUD (GET, POST, PUT, PATCH, DELETE)
* - Manejo de errores con ResponseStatusException
* - Validaciones manuales
* - Streams para filtrado de datos
*/
@RestController
public class LibroController {
/** Contador simple para generación de IDs automáticos */
private int contadorIds = 1;
/** Lista en memoria que simula una base de datos */
List<Libro> listaLibros = new ArrayList<>();
/**
* Obtiene todos los libros almacenados.
*
* GET /libros
*/
@GetMapping("/libros")
public List<Libro> listarLibros() {
return listaLibros;
}
/**
* Busca un libro por su identificador único.
*
* GET /libros/{id}
*
* @param id identificador del libro
* @return libro encontrado
*/
@GetMapping("/libros/{id}")
public Libro buscarLibroId(@PathVariable int id) {
return buscarPorId(id);
}
/**
* Método auxiliar reutilizable para búsqueda por ID.
*
* Lanza excepción 404 si el libro no existe.
*
* @param id identificador del libro
*/
private Libro buscarPorId(int id) {
return listaLibros.stream()
.filter(n -> n.getId() == id)
.findFirst()
.orElseThrow(() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND,
"Libro no encontrado"));
}
/**
* Filtra libros por autor.
*
* GET /libros/buscar?autor=Nombre
*
* @param autor nombre del autor
* @return lista de libros del autor
*/
@GetMapping("/libros/buscar")
public List<Libro> buscarAutor(@RequestParam String autor) {
return listaLibros.stream()
.filter(n -> n.getAutor().equalsIgnoreCase(autor))
.toList();
}
/**
* Crea un nuevo libro en el sistema.
*
* POST /libros
*
* Realiza validaciones básicas y comprueba duplicados.
*
* @param libro objeto recibido en el body
* @return libro creado con ID asignado
*/
@PostMapping("/libros")
public ResponseEntity<Libro> crearLibro(@RequestBody Libro libro) {
if (validaciones(libro) && duplicados(libro)) {
libro.setId(contadorIds++);
listaLibros.add(libro);
return ResponseEntity.status(HttpStatus.CREATED).body(libro);
}
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El libro no se creara");
}
/**
* Valida los campos obligatorios del libro.
*
* Reglas:
* - título obligatorio
* - autor obligatorio
* - páginas mayor que 0
*
* @param libro libro a validar
*/
private boolean validaciones(Libro libro) {
if (libro.getTitulo() == null || libro.getTitulo().isBlank()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El titulo esta en blanco.");
}
if (libro.getAutor() == null || libro.getAutor().isBlank()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El autor esta en blanco.");
}
if (libro.getPaginas() <= 0) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El libro no tiene paginas.");
}
return true;
}
/**
* Verifica que no existan títulos duplicados en memoria.
*
* @param libro libro a comprobar
*/
private boolean duplicados(Libro libro) {
for (Libro l : listaLibros) {
if (l.getTitulo().equalsIgnoreCase(libro.getTitulo())) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El libro esta duplicado.");
}
}
return true;
}
/**
* Actualiza completamente un libro existente.
*
* PUT /libros/{id}
*
* Sustituye todos los campos del libro.
*
* @param id identificador del libro
* @param libro datos nuevos del libro
*/
@PutMapping("/libros/{id}")
public ResponseEntity<Libro> actualizarLibro(@PathVariable int id,
@RequestBody Libro libro) {
Libro l = buscarLibroId(id);
if (validaciones(libro)) {
l.setTitulo(libro.getTitulo());
l.setAutor(libro.getAutor());
l.setPaginas(libro.getPaginas());
l.setDisponible(libro.isDisponible());
return ResponseEntity.status(HttpStatus.OK).body(l);
}
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"El libro no se actualizara");
}
/**
* Actualiza parcialmente la disponibilidad de un libro.
*
* PATCH /libros/{id}?disponible=true
*
* @param id identificador del libro
* @param disponible nuevo estado de disponibilidad
*/
@PatchMapping("/libros/{id}")
public Libro actualizarDisponibilidad(@PathVariable int id,
@RequestParam boolean disponible) {
Libro l = buscarLibroId(id);
l.setDisponible(disponible);
return l;
}
/**
* Elimina un libro por su ID.
*
* DELETE /libros/{id}
*
* @param id identificador del libro
* @return libro eliminado
*/
@DeleteMapping("/libros/{id}")
public ResponseEntity<Libro> borrarLibro(@PathVariable int id) {
Libro l = buscarLibroId(id);
listaLibros.remove(l);
return ResponseEntity.status(HttpStatus.OK).body(l);
}
}
package es.pulsoft.spring_libros.model;
public class Libro {
private int id;
private String titulo;
private String autor;
private int paginas;
private boolean disponible;
public Libro(){
}
public Libro(int id, String titulo, String autor, int paginas, boolean disponible) {
this.id = id;
this.titulo = titulo;
this.autor = autor;
this.paginas = paginas;
this.disponible = disponible;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getAutor() {
return autor;
}
public void setAutor(String autor) {
this.autor = autor;
}
public int getPaginas() {
return paginas;
}
public void setPaginas(int paginas) {
this.paginas = paginas;
}
public boolean isDisponible() {
return disponible;
}
public void setDisponible(boolean disponible) {
this.disponible = disponible;
}
}
API de Productos (CRUD avanzado en memoria)
Descripción
Desarrollo de una API REST en Spring Boot para la gestión de productos, implementando un CRUD avanzado en memoria con validaciones, filtros y ordenaciones mediante Streams.
El proyecto está diseñado sin capas adicionales (Service/Repository), centralizando toda la lógica en el controlador.
Conceptos trabajados
- Creación de controladores REST con @RestController.
- Uso de métodos HTTP: GET, POST, PUT, PATCH y DELETE.
- Manejo de ResponseEntity para respuestas HTTP controladas.
- Gestión de errores con ResponseStatusException.
- Uso de ArrayList como almacenamiento en memoria.
- Generación automática de identificadores.
- Uso de Streams para filtros, búsquedas y ordenaciones.
- Validaciones manuales de datos.
- Control de duplicados.
Entidad principal
La aplicación gestiona productos con la siguiente estructura:
- Identificador.
- Nombre.
- Categoría.
- Stock disponible.
- Precio.
- Estado activo/inactivo.
Almacenamiento
- List<Producto> listaProductos = new ArrayList<>();
- Control de IDs mediante contador automático.
Funcionalidades implementadas
- Listado completo de productos.
- Consulta de producto por ID.
- Creación de nuevos productos.
- Búsqueda por categoría.
- Filtrado por rango de stock.
- Filtrado por estado activo.
- Ordenación de productos por diferentes campos.
- Actualización completa de productos.
- Actualización parcial de campos específicos.
- Eliminación de productos.
Endpoints
- GET /productos → obtener todos los productos.
- GET /productos/{id} → obtener producto por ID.
- POST /productos → crear producto.
- GET /productos/buscar?categoria= → filtrar por categoría.
- GET /productos/stock?min=...&max=... → filtrar por stock.
- GET /productos/activos?estado= → filtrar por estado activo.
- GET /productos/ordenar?campo= → ordenar por campo.
- PUT /productos/{id} → actualización completa.
- PATCH /productos/{id} → actualización parcial.
- DELETE /productos/{id} → eliminar producto.
Validaciones aplicadas
- El nombre es obligatorio.
- La categoría es obligatoria.
- El stock no puede ser negativo.
- El precio debe ser mayor que 0.
- No se permiten nombres duplicados.
Gestión de errores
Se utiliza ResponseStatusException para controlar errores de validación, duplicados y recursos inexistentes, devolviendo códigos HTTP adecuados según cada caso.
package es.pulsoft.spring_productos.controller;
import es.pulsoft.spring_productos.model.Producto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
public class ProductoController {
// Contador simple para generar IDs únicos en memoria (simulación de base de datos)
private int contadorIds = 1;
// Lista en memoria que actúa como repositorio temporal de productos
private List<Producto> listaProductos = new ArrayList<>();
/**
* Obtiene todos los productos almacenados
*/
@GetMapping("/productos")
public List<Producto> listarProductos() {
return listaProductos;
}
/**
* Busca un producto por su ID
*/
@GetMapping("/productos/{id}")
public Producto buscarProducto(@PathVariable int id) {
return buscarPorId(id);
}
/**
* Método auxiliar para búsqueda de producto por ID
* Lanza excepción 404 si no existe
*/
private Producto buscarPorId(int id) {
return listaProductos.stream()
.filter(n -> n.getId() == id)
.findFirst()
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Producto no encontrado"));
}
/**
* Validaciones generales para creación/actualización completa (PUT)
* Reglas de negocio básicas:
* - nombre obligatorio
* - categoría obligatoria
* - stock >= 0
* - precio > 0
*/
private boolean validaciones(Producto producto) {
if (producto.getNombre() == null || producto.getNombre().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El nombre del producto no puede quedar vacio.");
}
if (producto.getCategoria() == null || producto.getCategoria().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"La categoria del producto no puede quedar vacia.");
}
if (producto.getStock() < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El stock no puede ser negativo.");
}
if (producto.getPrecio() <= 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El precio tiene que ser mayor que 0");
}
return true;
}
/**
* Validación de duplicados por nombre (case-insensitive)
* Usado en creación de productos
*/
private boolean duplicados(Producto producto) {
Producto p = listaProductos.stream()
.filter(n -> n.getNombre().equalsIgnoreCase(producto.getNombre()))
.findFirst()
.orElse(null);
if (p != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El Producto ya esta dado de alta.");
}
return true;
}
/**
* Crear un nuevo producto
*/
@PostMapping("/productos")
public ResponseEntity<Producto> crearProducto(@RequestBody Producto producto) {
if (validaciones(producto) && duplicados(producto)) {
producto.setId(contadorIds++);
listaProductos.add(producto);
return ResponseEntity.status(HttpStatus.CREATED).body(producto);
}
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Producto no creado");
}
/**
* Filtra productos por categoría
*/
@GetMapping("/productos/categoria")
public List<Producto> filtrarCategoria(@RequestParam String categoria) {
return listaProductos.stream()
.filter(n -> n.getCategoria().equalsIgnoreCase(categoria))
.toList();
}
/**
* Filtra productos por rango de stock
*/
@GetMapping("/productos/stock")
public List<Producto> filtrarStock(@RequestParam int minStock,
@RequestParam int maxStock) {
if (maxStock < minStock) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El stock maximo tiene que ser mayor que el minimo");
}
return listaProductos.stream()
.filter(n -> n.getStock() >= minStock && n.getStock() <= maxStock)
.toList();
}
/**
* Filtra productos por estado (activo/inactivo)
*/
@GetMapping("/productos/estado")
public List<Producto> filtrarEstado(@RequestParam boolean estado) {
return listaProductos.stream()
.filter(n -> n.isActivo() == estado)
.toList();
}
/**
* Ordena productos por un campo específico
*/
@GetMapping("/productos/ordenar")
public List<Producto> ordenarProductos(@RequestParam String campo) {
switch (campo.toLowerCase()) {
case "precio" -> {
return listaProductos.stream()
.sorted((a, b) -> Double.compare(a.getPrecio(), b.getPrecio()))
.toList();
}
case "stock" -> {
return listaProductos.stream()
.sorted((a, b) -> Integer.compare(a.getStock(), b.getStock()))
.toList();
}
default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El campo de ordenacion no es valido.");
}
}
/**
* Actualización completa de producto (PUT)
* Sustituye todos los campos del recurso
*/
@PutMapping("/productos/{id}")
public ResponseEntity<Producto> actualizarProducto(@PathVariable int id,
@RequestBody Producto producto) {
if (validaciones(producto) && duplicadosActualizaciones(id, producto)) {
Producto p = buscarPorId(id);
p.setNombre(producto.getNombre());
p.setCategoria(producto.getCategoria());
p.setStock(producto.getStock());
p.setPrecio(producto.getPrecio());
p.setActivo(producto.isActivo());
return ResponseEntity.status(HttpStatus.OK).body(p);
}
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Producto no actualizado");
}
/**
* Validación de duplicados en actualización (excluyendo el propio ID)
*/
private boolean duplicadosActualizaciones(int id, Producto producto) {
for (Producto p : listaProductos) {
if (p.getNombre().equalsIgnoreCase(producto.getNombre())
&& p.getId() != id) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El producto no se actualizara para evitar duplicados.");
}
}
return true;
}
/**
* Actualización parcial (PATCH)
* Permite modificar solo campos concretos del producto:
* - precio
* - stock
* - activo
*
* Usa Map para permitir actualización flexible sin DTO
*/
@PatchMapping("/productos/{id}")
public Producto actualizacionParcial(@RequestBody Map<String, Object> datos,
@PathVariable int id) {
Producto p = buscarPorId(id);
// Actualización de precio con validación
if (datos.containsKey("precio")) {
Object valor = datos.get("precio");
if (valor instanceof Number) {
double precio = ((Number) valor).doubleValue();
if (precio <= 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El precio tiene que ser mayor 0");
}
p.setPrecio(precio);
}
}
// Actualización de stock con validación
if (datos.containsKey("stock")) {
Object valor = datos.get("stock");
if (valor instanceof Number) {
int stock = ((Number) valor).intValue();
if (stock < 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El stock no puede ser negativo.");
}
p.setStock(stock);
}
}
// Actualización de estado activo/inactivo
if (datos.containsKey("activo")) {
Object valor = datos.get("activo");
if (valor instanceof Boolean) {
p.setActivo((Boolean) valor);
}
}
return p;
}
/**
* Elimina un producto por ID
*/
@DeleteMapping("/productos/{id}")
public ResponseEntity<Producto> borrarProducto(@PathVariable int id) {
Producto p = buscarPorId(id);
listaProductos.remove(p);
return ResponseEntity.status(HttpStatus.OK).body(p);
}
}
package es.pulsoft.spring_productos.model;
public class Producto {
private int id;
private String nombre;
private String categoria;
private int stock;
private double precio;
private boolean activo;
public Producto() {
}
public Producto(int id, String nombre, String categoria, int stock, double precio, boolean activo) {
this.id = id;
this.nombre = nombre;
this.categoria = categoria;
this.stock = stock;
this.precio = precio;
this.activo = activo;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getCategoria() {
return categoria;
}
public void setCategoria(String categoria) {
this.categoria = categoria;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
public double getPrecio() {
return precio;
}
public void setPrecio(double precio) {
this.precio = precio;
}
public boolean isActivo() {
return activo;
}
public void setActivo(boolean activo) {
this.activo = activo;
}
}
Gestión de clientes (CRUD básico sin base de datos)
Descripción
Desarrollo de una API REST en Spring Boot para la gestión de clientes, utilizando almacenamiento en memoria mediante una lista y aplicando operaciones CRUD completas con validaciones manuales.
El proyecto se implementa en un único controlador, sin capas adicionales de servicio o repositorio.
Conceptos trabajados
- Creación de controladores REST con @RestController.
- Uso de métodos HTTP: GET, POST, PUT, PATCH y DELETE.
- Manejo de listas en memoria (ArrayList).
- Validaciones manuales de datos.
- Gestión de errores con ResponseStatusException.
- Uso de ResponseEntity.
- Filtrado de datos mediante Streams.
- Actualización parcial de recursos.
Entidad principal
La aplicación gestiona clientes con la siguiente estructura:
- Identificador.
- Nombre.
- Email.
- Edad.
- Estado activo/inactivo.
Almacenamiento
- List<Cliente> listaClientes = new ArrayList<>();
- Generación de IDs automática mediante contador.
Funcionalidades implementadas
- Listado completo de clientes.
- Consulta de cliente por ID.
- Filtrado por rango de edad.
- Filtrado por estado activo/inactivo.
- Creación de clientes.
- Actualización completa de clientes.
- Actualización parcial de campos específicos.
- Eliminación de clientes.
Endpoints
- GET /clientes → listado de clientes.
- GET /clientes/{id} → cliente por ID.
- GET /clientes/edad?min=&max= → filtrado por edad.
- GET /clientes/activos?estado= → filtrado por estado.
- POST /clientes → creación de cliente.
- PUT /clientes/{id} → actualización completa.
- PATCH /clientes/{id} → actualización parcial.
- DELETE /clientes/{id} → eliminación de cliente.
Validaciones aplicadas
- El nombre no puede estar vacío.
- El email no puede estar vacío.
- La edad debe ser mayor que 0.
- No se permiten emails duplicados.
Gestión de errores
Se utiliza ResponseStatusException para controlar errores de validación, recursos inexistentes y duplicados, devolviendo códigos HTTP adecuados (400 y 404 según el caso).
package es.pulsoft.spring_clientes.controller;
import es.pulsoft.spring_clientes.model.Cliente;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Controlador REST encargado de la gestión de clientes en memoria.
* Incluye operaciones CRUD y filtros básicos.
*/
@RestController
public class ClienteController {
private int contadorIds = 1;
private final List<Cliente> listaClientes = new ArrayList<>();
/**
* Devuelve el listado completo de clientes.
*/
@GetMapping("/clientes")
public List<Cliente> listarClientes() {
return listaClientes;
}
/**
* Busca un cliente por su identificador.
*/
@GetMapping("/clientes/{id}")
public Cliente buscarCliente(@PathVariable int id) {
return buscarPorId(id);
}
/**
* Búsqueda interna por ID con control de error si no existe.
*/
private Cliente buscarPorId(int id) {
return listaClientes.stream()
.filter(n -> n.getId() == id)
.findFirst()
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND,
"Cliente no encontrado."));
}
/**
* Filtra clientes por rango de edad.
*/
@GetMapping("/clientes/edad/")
public List<Cliente> filtrarEdad(@RequestParam int minEdad,
@RequestParam int maxEdad) {
if (maxEdad < minEdad) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"La edad máxima no puede ser inferior a la mínima.");
}
return listaClientes.stream()
.filter(n -> n.getEdad() >= minEdad && n.getEdad() <= maxEdad)
.toList();
}
/**
* Filtra clientes por estado activo/inactivo.
*/
@GetMapping("/clientes/activos/")
public List<Cliente> filtrarActivos(@RequestParam boolean activo) {
return listaClientes.stream()
.filter(n -> n.isActivo() == activo)
.toList();
}
/**
* Validaciones básicas del cliente antes de persistir o actualizar.
*/
private boolean validaciones(Cliente cliente) {
if (cliente.getNombre() == null || cliente.getNombre().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El nombre del cliente no puede quedar vacío.");
}
if (cliente.getEmail() == null || cliente.getEmail().isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El email del cliente no puede quedar vacío.");
}
if (cliente.getEdad() <= 0) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"La edad del cliente no puede ser menor de 1.");
}
return true;
}
/**
* Comprueba que no exista otro cliente con el mismo email.
*/
private boolean duplicados(Cliente cliente) {
listaClientes.stream()
.filter(n -> n.getEmail().equalsIgnoreCase(cliente.getEmail()))
.findFirst()
.ifPresent(n -> {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Cliente con email duplicado.");
});
return true;
}
/**
* Alta de nuevo cliente.
*/
@PutMapping("/clientes")
public ResponseEntity<Cliente> agregarCliente(@RequestBody Cliente cliente) {
if (validaciones(cliente) && duplicados(cliente)) {
cliente.setId(contadorIds++);
listaClientes.add(cliente);
return ResponseEntity.ok(cliente);
}
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Cliente no creado.");
}
/**
* Validación de duplicados en actualización (excluyendo el propio ID).
*/
private boolean duplicadosActualizar(int id, Cliente cliente) {
listaClientes.stream()
.filter(n -> n.getEmail().equalsIgnoreCase(cliente.getEmail())
&& n.getId() != id)
.findFirst()
.ifPresent(n -> {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"El email ya pertenece a otro cliente.");
});
return true;
}
/**
* Actualización completa del cliente.
*/
@PutMapping("/clientes/{id}")
public ResponseEntity<Cliente> actualizarCliente(@PathVariable int id,
@RequestBody Cliente cliente) {
if (validaciones(cliente) && duplicadosActualizar(id, cliente)) {
Cliente c = buscarCliente(id);
c.setNombre(cliente.getNombre());
c.setEmail(cliente.getEmail());
c.setEdad(cliente.getEdad());
c.setActivo(cliente.isActivo());
return ResponseEntity.ok(c);
}
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Cliente no actualizado.");
}
/**
* Actualización parcial de campos del cliente.
*/
@PatchMapping("/clientes/{id}")
public ResponseEntity<Cliente> actualizacionParcial(@PathVariable int id,
@RequestBody Map<String, Object> cliente) {
Cliente c = buscarPorId(id);
if (cliente.containsKey("email")) {
Object valor = cliente.get("email");
if (valor instanceof String) {
c.setEmail((String) valor);
}
}
if (cliente.containsKey("edad")) {
Object valor = cliente.get("edad");
if (valor instanceof Number) {
c.setEdad(((Number) valor).intValue());
}
}
if (cliente.containsKey("activo")) {
Object valor = cliente.get("activo");
if (valor instanceof Boolean) {
c.setActivo((Boolean) valor);
}
}
return ResponseEntity.ok(c);
}
/**
* Eliminación de cliente por ID.
*/
@DeleteMapping("/clientes/{id}")
public ResponseEntity<Cliente> borrarCliente(@PathVariable int id) {
Cliente c = buscarPorId(id);
listaClientes.remove(c);
return ResponseEntity.ok(c);
}
}
package es.pulsoft.spring_clientes.model;
public class Cliente {
private int id;
private String nombre;
private String email;
private int edad;
private boolean activo;
public Cliente() {
}
public Cliente(int id, String nombre, String email, int edad, boolean activo) {
this.id = id;
this.nombre = nombre;
this.email = email;
this.edad = edad;
this.activo = activo;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public int getEdad() {
return edad;
}
public void setEdad(int edad) {
this.edad = edad;
}
public boolean isActivo() {
return activo;
}
public void setActivo(boolean activo) {
this.activo = activo;
}
}