Ir al contenido principal

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:

  1. Capa de controladores: Maneja las peticiones HTTP
  2. Capa de servicios: Contiene la lógica de negocio
  3. Capa de repositorios: Gestiona el acceso a datos
  4. 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:

  1. Añadir paginación: Para manejar grandes cantidades de posts y comentarios
  2. Implementar JWT: Para una autenticación más robusta
  3. Añadir categorías o etiquetas: Para organizar el contenido
  4. Implementar búsquedas: Usando Spring Data JPA con consultas personalizadas
  5. Añadir caché: Para mejorar el rendimiento
  6. 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.