Proyecto: API REST para un blog
Introducción
Tras explorar los fundamentos del desarrollo web con Java y los servicios RESTful, es momento de aplicar estos conocimientos en un proyecto práctico: la creación de una API REST para un blog. Este proyecto nos permitirá integrar múltiples conceptos aprendidos durante el curso, desde la programación orientada a objetos hasta el desarrollo de servicios web, pasando por la persistencia de datos. Una API REST bien diseñada es fundamental en el desarrollo moderno, ya que permite la comunicación entre diferentes sistemas y facilita la creación de aplicaciones web y móviles.
En este artículo, desarrollaremos paso a paso una API REST completa para gestionar un blog, incluyendo la creación, lectura, actualización y eliminación de entradas (operaciones CRUD). Utilizaremos Spring Boot como framework principal, aprovechando su capacidad para simplificar el desarrollo de aplicaciones Java.
Planificación del proyecto
Antes de empezar a programar, es importante definir claramente el alcance del proyecto:
Funcionalidades
- Gestión de publicaciones (posts)
- Gestión de comentarios
- Gestión de usuarios/autores
- Autenticación básica
Estructura de la aplicación
Seguiremos una arquitectura en capas:
- Capa de controladores: Maneja las peticiones HTTP
- Capa de servicios: Contiene la lógica de negocio
- Capa de repositorios: Gestiona el acceso a datos
- Capa de modelos: Define las entidades de negocio
Configuración del proyecto
Requisitos previos
- JDK 21 instalado
- Maven o Gradle para gestionar dependencias
- Una IDE como IntelliJ IDEA, Eclipse o VS Code
Creación del proyecto con Spring Boot
Podemos crear un proyecto Spring Boot utilizando Spring Initializr. Seleccionamos:
- Proyecto: Maven
- Lenguaje: Java
- Versión de Spring Boot: 3.2.x o superior
- Grupo: com.ejemplojava
- Artefacto: blogapi
- Dependencias:
- Spring Web
- Spring Data JPA
- H2 Database (para desarrollo)
- Spring Security (opcional para autenticación)
- Validation
Descargamos el proyecto, lo descomprimimos y lo importamos en nuestra IDE.
Implementación del modelo de datos
Definición de entidades
Comencemos creando las entidades principales:
// Archivo: Post.java
package com.ejemplojava.blogapi.modelo;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 100)
private String titulo;
@NotBlank
@Size(max = 5000)
@Column(columnDefinition = "TEXT")
private String contenido;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "autor_id", nullable = false)
private Usuario autor;
private LocalDateTime fechaCreacion = LocalDateTime.now();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comentario> comentarios = new ArrayList<>();
// Getters, setters y constructores
public Post() {
}
public Post(String titulo, String contenido, Usuario autor) {
this.titulo = titulo;
this.contenido = contenido;
this.autor = autor;
}
// Getters y setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getContenido() {
return contenido;
}
public void setContenido(String contenido) {
this.contenido = contenido;
}
public Usuario getAutor() {
return autor;
}
public void setAutor(Usuario autor) {
this.autor = autor;
}
public LocalDateTime getFechaCreacion() {
return fechaCreacion;
}
public void setFechaCreacion(LocalDateTime fechaCreacion) {
this.fechaCreacion = fechaCreacion;
}
public List<Comentario> getComentarios() {
return comentarios;
}
public void setComentarios(List<Comentario> comentarios) {
this.comentarios = comentarios;
}
}
// Archivo: Usuario.java
package com.ejemplojava.blogapi.modelo;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 50)
private String nombre;
@NotBlank
@Email
@Size(max = 100)
@Column(unique = true)
private String email;
@NotBlank
@Size(max = 120)
private String password;
@OneToMany(mappedBy = "autor", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();
// Getters, setters y constructores
public Usuario() {
}
public Usuario(String nombre, String email, String password) {
this.nombre = nombre;
this.email = email;
this.password = password;
}
// Getters y setters
public Long getId() {
return id;
}
public void setId(Long 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 String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<Post> getPosts() {
return posts;
}
public void setPosts(List<Post> posts) {
this.posts = posts;
}
}
// Archivo: Comentario.java
package com.ejemplojava.blogapi.modelo;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
@Entity
@Table(name = "comentarios")
public class Comentario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 1000)
@Column(columnDefinition = "TEXT")
private String texto;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "usuario_id", nullable = false)
private Usuario usuario;
private LocalDateTime fechaCreacion = LocalDateTime.now();
// Getters, setters y constructores
public Comentario() {
}
public Comentario(String texto, Post post, Usuario usuario) {
this.texto = texto;
this.post = post;
this.usuario = usuario;
}
// Getters y setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTexto() {
return texto;
}
public void setTexto(String texto) {
this.texto = texto;
}
public Post getPost() {
return post;
}
public void setPost(Post post) {
this.post = post;
}
public Usuario getUsuario() {
return usuario;
}
public void setUsuario(Usuario usuario) {
this.usuario = usuario;
}
public LocalDateTime getFechaCreacion() {
return fechaCreacion;
}
public void setFechaCreacion(LocalDateTime fechaCreacion) {
this.fechaCreacion = fechaCreacion;
}
}
Creación de repositorios
Ahora crearemos los repositorios que permitirán acceder a los datos:
// Archivo: PostRepository.java
package com.ejemplojava.blogapi.repositorio;
import com.ejemplojava.blogapi.modelo.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByAutorId(Long autorId);
}
// Archivo: UsuarioRepository.java
package com.ejemplojava.blogapi.repositorio;
import com.ejemplojava.blogapi.modelo.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
Optional<Usuario> findByEmail(String email);
Boolean existsByEmail(String email);
}
// Archivo: ComentarioRepository.java
package com.ejemplojava.blogapi.repositorio;
import com.ejemplojava.blogapi.modelo.Comentario;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ComentarioRepository extends JpaRepository<Comentario, Long> {
List<Comentario> findByPostId(Long postId);
}
Implementación de la capa de servicio
La capa de servicio contiene la lógica de negocio de nuestra aplicación:
// Archivo: PostService.java
package com.ejemplojava.blogapi.servicio;
import com.ejemplojava.blogapi.modelo.Post;
import com.ejemplojava.blogapi.repositorio.PostRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PostService {
@Autowired
private PostRepository postRepository;
public List<Post> obtenerTodos() {
return postRepository.findAll();
}
public Post obtenerPorId(Long id) {
return postRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Post no encontrado con id: " + id));
}
public List<Post> obtenerPorAutor(Long autorId) {
return postRepository.findByAutorId(autorId);
}
public Post crear(Post post) {
return postRepository.save(post);
}
public Post actualizar(Long id, Post postRequest) {
Post post = obtenerPorId(id);
post.setTitulo(postRequest.getTitulo());
post.setContenido(postRequest.getContenido());
return postRepository.save(post);
}
public void eliminar(Long id) {
Post post = obtenerPorId(id);
postRepository.delete(post);
}
}
Implementamos servicios similares para Usuario
y Comentario
.
Implementación de los controladores REST
Los controladores exponen los endpoints REST para interactuar con nuestra API:
// Archivo: PostController.java
package com.ejemplojava.blogapi.controlador;
import com.ejemplojava.blogapi.modelo.Post;
import com.ejemplojava.blogapi.servicio.PostService;
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/posts")
public class PostController {
@Autowired
private PostService postService;
@GetMapping
public ResponseEntity<List<Post>> obtenerTodos() {
List<Post> posts = postService.obtenerTodos();
return new ResponseEntity<>(posts, HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Post> obtenerPorId(@PathVariable Long id) {
Post post = postService.obtenerPorId(id);
return new ResponseEntity<>(post, HttpStatus.OK);
}
@GetMapping("/autor/{autorId}")
public ResponseEntity<List<Post>> obtenerPorAutor(@PathVariable Long autorId) {
List<Post> posts = postService.obtenerPorAutor(autorId);
return new ResponseEntity<>(posts, HttpStatus.OK);
}
@PostMapping
public ResponseEntity<Post> crear(@Valid @RequestBody Post post) {
Post nuevoPost = postService.crear(post);
return new ResponseEntity<>(nuevoPost, HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<Post> actualizar(@PathVariable Long id, @Valid @RequestBody Post post) {
Post postActualizado = postService.actualizar(id, post);
return new ResponseEntity<>(postActualizado, HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
postService.eliminar(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Implementamos controladores similares para Usuario
y Comentario
.
Configuración de Spring Security (opcional)
Para añadir autenticación básica a nuestra API:
// Archivo: SecurityConfig.java
package com.ejemplojava.blogapi.configuracion;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.httpBasic();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
DTOs (Data Transfer Objects)
Podemos crear DTOs para separar la representación externa de nuestras entidades:
// Archivo: PostDTO.java
package com.ejemplojava.blogapi.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
public class PostDTO {
private Long id;
@NotBlank
@Size(max = 100)
private String titulo;
@NotBlank
@Size(max = 5000)
private String contenido;
private Long autorId;
private String nombreAutor;
private LocalDateTime fechaCreacion;
private int numeroComentarios;
// Getters y setters
// Constructor vacío
public PostDTO() {
}
// Getters y setters completos
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getContenido() {
return contenido;
}
public void setContenido(String contenido) {
this.contenido = contenido;
}
public Long getAutorId() {
return autorId;
}
public void setAutorId(Long autorId) {
this.autorId = autorId;
}
public String getNombreAutor() {
return nombreAutor;
}
public void setNombreAutor(String nombreAutor) {
this.nombreAutor = nombreAutor;
}
public LocalDateTime getFechaCreacion() {
return fechaCreacion;
}
public void setFechaCreacion(LocalDateTime fechaCreacion) {
this.fechaCreacion = fechaCreacion;
}
public int getNumeroComentarios() {
return numeroComentarios;
}
public void setNumeroComentarios(int numeroComentarios) {
this.numeroComentarios = numeroComentarios;
}
}
Manejo de excepciones global
Creamos un manejador de excepciones para toda la aplicación:
// Archivo: GlobalExceptionHandler.java
package com.ejemplojava.blogapi.excepcion;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> manejarEntidadNoEncontrada(EntityNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("mensaje", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> manejarValidacion(MethodArgumentNotValidException ex) {
Map<String, String> errores = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String campo = ((FieldError) error).getField();
String mensaje = error.getDefaultMessage();
errores.put(campo, mensaje);
});
return new ResponseEntity<>(errores, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> manejarExcepcionGeneral(Exception ex) {
Map<String, String> error = new HashMap<>();
error.put("mensaje", "Se produjo un error inesperado");
error.put("detalle", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Propiedades de la aplicación
Configuramos application.properties
:
# Configuración de la base de datos H2
spring.datasource.url=jdbc:h2:mem:blogdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# Consola H2 para desarrollo
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Configuración JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# Configuraciones de servidor
server.port=8080
Prueba de la API
Para probar nuestra API, podemos usar herramientas como Postman o curl:
Crear un usuario
curl -X POST http://localhost:8080/api/usuarios \
-H "Content-Type: application/json" \
-d '{"nombre":"María López","email":"maria@ejemplo.com","password":"clave123"}'
Crear una entrada de blog
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"titulo":"Mi primer post","contenido":"Este es el contenido de mi primer post","autorId":1}'
Obtener todas las entradas
curl -X GET http://localhost:8080/api/posts
Ampliaciones posibles
Algunas formas de mejorar nuestra API REST para blog:
- Añadir paginación: Para manejar grandes cantidades de posts y comentarios
- Implementar JWT: Para una autenticación más robusta
- Añadir categorías o etiquetas: Para organizar el contenido
- Implementar búsquedas: Usando Spring Data JPA con consultas personalizadas
- Añadir caché: Para mejorar el rendimiento
- Implementar documentación con Swagger/OpenAPI: Para documentar los endpoints
Resumen
En este proyecto hemos desarrollado una API REST completa para un blog utilizando Spring Boot. Hemos implementado operaciones CRUD para posts, usuarios y comentarios, siguiendo buenas prácticas de desarrollo como la arquitectura en capas, el uso de DTOs y el manejo global de excepciones.
Este proyecto integra muchos de los conceptos que hemos aprendido a lo largo del curso: programación orientada a objetos, manejo de colecciones, acceso a bases de datos y desarrollo web con Java. La API REST que hemos creado puede servir como backend para una aplicación web o móvil de blog, demostrando cómo Java puede utilizarse en aplicaciones modernas y distribuidas.
En el siguiente proyecto, exploraremos cómo desarrollar una aplicación completa que integre una interfaz gráfica con una base de datos, llevando nuestros conocimientos de Java a un nivel superior.