Antes de ponernos a escribir código, es importante definir el estilo arquitectónico de la aplicación, sus componentes principales y cómo interactúan entre sí. Cualquier programador experimentado confirmará que una arquitectura mal diseñada es como una casa sin cimientos: puede aguantar, pero no por mucho.
1. Visión arquitectónica
Elección del estilo arquitectónico
Desarrollamos la aplicación con una arquitectura monolítica. Sí, sí, nada de hype con microservicios en esta fase. ¿Por qué? Porque nuestra aplicación será relativamente pequeña, y pasarse a microservicios ahora no tiene sentido. Todo estará en un solo proyecto, pero lo dividiremos en módulos lógicos.
Componentes de la aplicación
Los elementos de nuestra aplicación se pueden dividir en varios bloques clave:
- REST API: vamos a ofrecer una interfaz de interacción vía HTTP requests para usuarios externos (por ejemplo, el frontend).
- Capa de servicio (business logic): aquí concentraremos la funcionalidad principal de la aplicación, como el procesamiento de datos, validación y gestión de transacciones.
- Base de datos: para el almacenamiento de la información.
- Seguridad: protegeremos el acceso a nuestros datos y funcionalidades.
Aquí tienes un esquema del componente:
+-------------------+
| REST API |
|-------------------|
| Controladores |
+-------------------+
|
v
+-------------------+
| Capa de servicio |
|-------------------|
| Lógica de negocio|
+-------------------+
|
v
+-------------------+
| Acceso a datos |
|-------------------|
| Repositorios JPA |
+-------------------+
|
v
+-------------------+
| Base de datos |
|-------------------|
2. Diseño REST API
Para empezar, recordemos que REST API —es la interfaz que permite interactuar con la aplicación vía HTTP requests. Principios principales de REST:
- Orientación a recursos: todo en nuestra aplicación se considera un recurso. Por ejemplo, un usuario es el recurso
/users. - HTTP methods: para distintas operaciones se usan diferentes métodos:
GETpara obtener datos.POSTpara crear nuevos datos.PUTpara actualizar datos.DELETEpara eliminar datos.
- Códigos de estado HTTP: el servidor devuelve el estado de la operación. Por ejemplo:
200 OKpara respuesta exitosa.201 Createdpara creación exitosa del recurso.404 Not Foundpara recurso no encontrado.
Diseño de endpoints
Pensamos crear un CRUD básico para usuarios. Estos son los endpoints previstos:
| Método | URL | Descripción |
|---|---|---|
| GET | /users |
Obtener la lista de usuarios |
| GET | /users/{id} |
Obtener un usuario por ID |
| POST | /users |
Crear un nuevo usuario |
| PUT | /users/{id} |
Actualizar los datos de un usuario |
| DELETE | /users/{id} |
Eliminar un usuario |
Así se ve en el controlador:
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public List<User> getAllUsers() {
// Devuelve la lista de todos los usuarios
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
// Obtiene el usuario por id
}
@PostMapping
public User createUser(@RequestBody User user) {
// Crea un nuevo usuario
}
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
// Actualiza los datos del usuario
}
@DeleteMapping("/{id}")
public ResponseEntity
deleteUser(@PathVariable Long id) {
// Elimina el usuario
}
}
3. Seguridad de la aplicación
Implementación de autenticación usando Spring Security
Ya sabéis que Spring Security nos permite añadir reglas de autenticación y autorización.
1. Inclusión de la dependencia: En pom.xml añadimos:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Creación de la configuración de seguridad:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/**").authenticated()
.anyRequest().permitAll()
.and()
.httpBasic(); // Usamos autenticación básica
}
}
Aquí protegemos los endpoints /users/**, y dejamos el resto accesible para todos.
4. Trabajo con la base de datos
Diseño de la base de datos
Para nuestra aplicación necesitaremos la tabla users. Ejemplo de estructura (diagrama ER):
| Campo | Tipo de datos | Descripción |
|---|---|---|
| id | BIGINT | Identificador |
| name | VARCHAR | Nombre de usuario |
| VARCHAR | Correo electrónico | |
| password | VARCHAR | Contraseña |
Implementación de entidades
Crearemos la entidad User usando anotaciones JPA:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
// Getters y setters
}
Repositorio para trabajar con la base
Usamos Spring Data JPA para operar con los datos:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Ahora podemos realizar operaciones CRUD con los datos de usuarios a través de esta interfaz.
Configuración de la conexión
En application.properties configuramos la conexión a la base:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
Interacción entre componentes
Entonces, ¿cómo se conecta todo esto?
- La petición REST llega al controlador.
- El controlador llama al método correspondiente de la capa de servicio.
- La capa de servicio interactúa con los repositorios y devuelve el resultado.
- El controlador devuelve la respuesta al cliente.
Ejemplo de la cadena:
@RestController
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
}
}
En esta etapa la arquitectura de la aplicación está lista. A continuación nos meteremos en la descomposición de las tareas del proyecto para entender cómo organizar mejor el trabajo en los módulos restantes.
GO TO FULL VERSION