Tema 1 — Arquitectura SPA
Arquitectura de dos servidores
Una SPA separa frontend (recursos estáticos) y backend (API REST):
| Servidor web | Sirve index.html, bundle JS, CSS, imágenes. Sin lógica de negocio. Puerto 5173 en dev. |
| Servidor de app | Spring Boot. Expone API REST JSON. No genera HTML. Puerto 8080. |
| Navegador | Descarga index.html + JS bundle → React arranca → peticiones AJAX al backend. |
Spring Boot
@SpringBootApplication // = @Configuration + @ComponentScan + @EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
| mvn spring-boot:run | Desarrollo. Arranca con classpath de Maven. |
| java -jar app.jar | Producción. Fat JAR con Tomcat embebido incluido. |
El fat JAR contiene Tomcat embebido → NO necesita un servidor de aplicaciones externo. Solo java -jar. (Pregunta 10b del examen: "instalarlo en servidor de aplicaciones Java" → FALSO)
application.yml
spring:
datasource:
url: jdbc:mysql://localhost/pa
hikari:
maximum-pool-size: 5 # Para tests: 1 (evita deadlocks)
jpa:
hibernate:
ddl-auto: none # No modifica el esquema
open-in-view: false # Recomendado
YAML: espacios, nunca tabuladores. application-test.yml SOBREESCRIBE propiedades de application.yml — NO lo reemplaza. Ambos se cargan.
Vite / Frontend
npm run dev # Dev server → localhost:5173 (HMR)
npm run build # Genera dist/ (bundle optimizado para producción)
npm run lint # ESLint
Tema 2 — JPA: Entidades y Relaciones ★ EXAMEN
Entidades básicas
@Entity
@Table(name = "Product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "productName")
private String name;
// JPA accede VÍA GETTERS (acceso por propiedad)
public Long getId() { return id; }
public String getName() { return name; }
}
Regla fundamental de las relaciones ★ EXAMEN
REGLA DE ORO — memorizar:
- Owning side (lado dueño) = el que tiene la FK en la tabla = siempre el lado N en 1:N =
@ManyToOne + @JoinColumn - Inverse side (lado inverso) = el lado 1 en 1:N =
@OneToMany(mappedBy=...) - mappedBy SOLO va en @OneToMany. Nunca en @ManyToOne.
- @JoinColumn SOLO va en el lado dueño (ManyToOne). Nunca en el lado OneToMany.
Relación 1:N bidireccional — código correcto
// ── Lado N (owning side) — TIENE la FK en su tabla ──────────────────
@Entity
public class Account {
@ManyToOne // ← @ManyToOne en el lado N
@JoinColumn(name = "userId") // ← @JoinColumn indica la columna FK
private User user;
public User getUser() { return user; }
}
// ── Lado 1 (inverse side) — NO tiene FK ──────────────────────────────
@Entity
public class User {
@OneToMany(mappedBy = "user") // ← mappedBy = nombre del campo en Account
private Set<Account> accounts = new HashSet<>();
public Set<Account> getAccounts() { return accounts; }
}
User-Account bidireccional 1:N. La tabla Account tiene columna userId (FK). ¿Cuál anotación es correcta?
Relación 1:1
// User tiene bankCardId (FK) → User es el owning side
@Entity
public class User {
@OneToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "bankCardId") // FK en tabla User
private BankCard bankCard;
public BankCard getBankCard() { return bankCard; }
}
En 1:1 igual que en N:1: @JoinColumn va en el lado que TIENE la FK. Si la tabla User tiene bankCardId, @JoinColumn va en User.getBankCard(), NO en BankCard con name="userId".
FetchType por defecto
| Anotación | FetchType por defecto |
|---|---|
@ManyToOne | EAGER |
@OneToOne | EAGER |
@OneToMany | LAZY |
@ManyToMany | LAZY |
Spring Data CrudRepository
public interface ProductDao extends CrudRepository<Product, Long> {
// Spring genera el SQL automáticamente por nombre del método:
List<Product> findByCategoryIdOrderByName(Long categoryId);
Optional<Product> findById(Long id);
// Devuelve Optional → usar .get() o .orElse(null)
}
Paginación con Slice
Slice<Product> findByCategoryId(Long catId, Pageable pageable);
Pageable pageable = PageRequest.of(0, 10); // página 0, tamaño 10
Slice<Product> slice = productDao.findByCategoryId(1L, pageable);
List<Product> items = slice.getContent();
boolean hasMore = slice.hasNext();
// Slice NO hace COUNT(*) — solo comprueba si hay +1 elemento más
JPQL
// JPQL usa nombres de ENTIDADES y PROPIEDADES (no tablas/columnas)
@Query("SELECT p FROM Product p WHERE p.category.id = ?1 AND p.name LIKE ?2")
List<Product> find(Long catId, String kw);
@Query("SELECT p FROM Product p WHERE p.name LIKE :kw ORDER BY p.name")
Slice<Product> findByKeyword(@Param("kw") String keyword, Pageable p);
DAO personalizado
// 1. Interfaz con métodos custom
public interface CustomizedProductDao {
Slice<Product> findByCriteria(ProductSearchCriteria criteria, int page);
}
// 2. Implementación — sufijo "Impl" obligatorio
public class CustomizedProductDaoImpl implements CustomizedProductDao {
@PersistenceContext
private EntityManager em;
}
// 3. DAO principal extiende ambos
public interface ProductDao
extends CrudRepository<Product, Long>, CustomizedProductDao { }
Tema 2 — JPA: LAZY, queries, tests ★ EXAMEN
FetchType.LAZY — cuándo se lanza la consulta SQL ★ EXAMEN
getId() sobre un proxy LAZY NO lanza consulta. El ID ya está disponible en el proxy sin cargar el objeto. La consulta se lanza al acceder a CUALQUIER otra propiedad (getName(), getPrice(), etc.).
Event event = eventDao.findById(id).get(); // [A] query para Event
Category category = event.getCategory(); // [B] devuelve proxy, SIN query
Long id2 = category.getId(); // [C] NO lanza query (ID en proxy)
String name = category.getName(); // [D] AQUÍ se lanza la query
Event→Category LAZY. ¿En qué línea se lanza la consulta a BD?
LAZY en colecciones
Order order = orderDao.findById(id).get(); // [1] query para Order
Set<OrderItem> items = order.getOrderItems(); // [2] proxy de colección, SIN query
for (OrderItem item : items) { // [3] AQUÍ se lanza query de OrderItems
item.getId(); // [4] no query (ya cargado)
item.getDate(); // [5] no query (ya cargado)
Product p = item.getProduct(); // [6] proxy Product, SIN query
p.getId(); // [7] no query (ID en proxy)
p.getName(); // [8] AQUÍ se lanza query de Product
}
Order→OrderItems→Product todos LAZY. ¿Qué líneas causan consulta a BD?
@Version — Optimistic Locking ★ EXAMEN
@Entity
public class ShoppingCart {
@Version
private int version; // Hibernate gestiona automáticamente
}
// UPDATE ShoppingCart SET ..., version=2 WHERE id=? AND version=1
// Si 0 filas actualizadas → alguien modificó antes → OptimisticLockException
// OptimisticLockException extiende RuntimeException → ROLLBACK automático
Dos usuarios ejecutan concurrentemente el mismo caso de uso sobre la misma entidad. ¿Cuál afirmación es correcta?
Tests de integración ★ EXAMEN
@SpringBootTest
@ActiveProfiles("test") // Activa perfil test → carga application.yml + application-test.yml
@Transactional // ← Auto-rollback tras cada @Test
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testSignUpAndLoginFromId() throws ... {
User user = new User("login", "pass", "nombre", "apellido", "email");
userService.signUp(user);
User user2 = userService.loginFromId(user.getId());
assertEquals(user, user2); // Funciona SIN redefinir equals/hashCode
}
}
4 trampas sobre tests (Pregunta 4):
- NO hay que redefinir
equals/hashCode: Hibernate devuelve el mismo objeto Java en la misma sesión (caché de primer nivel) →==funciona. - NO hay que eliminar el usuario manualmente:
@Transactionalen el test hace rollback automático tras cada test. @ActiveProfiles("test")cargaapplication.ymlYapplication-test.yml. El de test sobreescribe propiedades del principal, NO lo reemplaza.maximum-pool-size: 1en tests para evitar deadlocks.
Test con @SpringBootTest, @ActiveProfiles("test") y @Transactional. ¿Qué afirmación es correcta?
Tema 3 — Lógica de Negocio ★ EXAMEN
Inyección de dependencias
@Service // ← Bean de servicio (singleton)
public class CatalogService {
private final ProductDao productDao;
@Autowired // ← Inyección (preferible en constructor)
public CatalogService(ProductDao productDao) {
this.productDao = productDao;
}
}
@Repository // ← Bean de DAO. Activa traducción de excepciones
public interface ProductDao extends CrudRepository<Product, Long> { }
@Service va en la clase de servicio. @Autowired va en el punto de inyección (campo, constructor o setter). Son cosas distintas. (Pregunta 5a: "accountService debería anotarse con @Service en lugar de @Autowired" → FALSO)
@Transactional — semántica exacta ★ EXAMEN
Regla de rollback — memorizar:
- Checked exception (Exception, IOException, InstanceNotFoundException...) → COMMIT
- Unchecked exception (RuntimeException, Error, OptimisticLockException...) → ROLLBACK
- Para rollback con checked:
@Transactional(rollbackFor = MiException.class)
@Transactional
public void buy(Long cartId) throws InputValidationException {
// Si lanza InputValidationException (checked) → COMMIT
// Si lanza RuntimeException → ROLLBACK
}
@Transactional(readOnly = true) // SÍ corre dentro de una transacción (solo lectura)
public ProductDto findProduct(Long id) throws NotFoundException { ... }
@Transactional — propagación ★ EXAMEN
Por defecto la propagación es REQUIRED: si ya existe una transacción activa, la segunda operación se une a ella (no crea una nueva). Si no existe, crea una nueva.
Servicio anotado con @Transactional. ¿Cuál afirmación es correcta?
Domain Model — @Transient
@Entity
public class ShoppingCart {
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<ShoppingItem> items;
@Transient // ← NO se persiste en BD, es método de negocio
public BigDecimal getTotalPrice() {
return items.stream()
.map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
Tema 4 — REST Controllers ★ EXAMEN
Anotaciones REST
@RestController // = @Controller + @ResponseBody en todos los métodos
@RequestMapping("/catalog")
public class CatalogController {
@Autowired
private CatalogService catalogService;
// GET /catalog/products?categoryId=1&keywords=matrix&page=0
@GetMapping("/products")
public ProductSearchResultDto findProducts(
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false, defaultValue = "") String keywords,
@RequestParam(defaultValue = "0") int page) { ... }
// GET /catalog/products/42
@GetMapping("/products/{id}")
public ProductDto findById(@PathVariable Long id, Locale locale) { ... }
// POST /catalog/products — body JSON → DTO
@PostMapping("/products")
@ResponseStatus(HttpStatus.CREATED) // 201
public ProductDto add(
@Validated @RequestBody ProductDto dto) { ... }
// ↑ valida ↑ deserializa body JSON ← EXAMEN
}
@RequestBody — crítico ★ EXAMEN
Sin @RequestBody Spring no lee el cuerpo de la petición. @RequestBody le dice a Jackson que deserialice el JSON del body al DTO. Si falta → el parámetro será null o Spring lanzará error.
POST /accounts/transfer con @Validated TransferParamsDto params. ¿Qué falta?
Parámetros REST — tipos
| Anotación | Origen | Ejemplo |
|---|---|---|
@PathVariable | URL: /products/42 | @GetMapping("/products/{id}") |
@RequestParam | Query string: ?page=0 | @RequestParam(defaultValue="0") int page |
@RequestBody | Body JSON | @RequestBody ProductDto dto |
@RequestAttribute | Atributo de request (set por filtros) | Usado por JwtFilter para pasar userId |
Bean Validation
public class BuyParamsDto {
@NotNull
private Long shoppingCartId;
@NotBlank @Size(min=1, max=100)
private String postalAddress;
@NotBlank @Size(min=1, max=20)
private String postalCode;
}
@PostMapping("/orders")
public OrderDto buy(@Validated @RequestBody BuyParamsDto dto) {
// Si hay errores → MethodArgumentNotValidException → 400 Bad Request automático
}
Manejo de excepciones
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InstanceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 404
@ResponseBody
public ErrorsDto handleNotFound(InstanceNotFoundException e) {
return new ErrorsDto(e.getMessage());
}
@ExceptionHandler(InputValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 400
@ResponseBody
public ErrorsDto handleValidation(InputValidationException e) { ... }
}
Internacionalización
@GetMapping("/products/{id}")
public ProductDto find(@PathVariable Long id, Locale locale) throws ... {
// locale inyectado por Spring desde el header Accept-Language
}
@Autowired private MessageSource messageSource;
String msg = messageSource.getMessage("project.error.notFound", null, locale);
Tema 4 — JWT y Spring Security ★ EXAMEN
JWT — estructura ★ EXAMEN
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJVU0VSIn0.firma
└─── HEADER (base64url) ──────────────┘ └── PAYLOAD ─────────────────────────┘ └sig┘
Header → {"alg":"HS256","typ":"JWT"} — algoritmo de firma
Payload → {"userId":1,"role":"USER","exp":...} — NO cifrado, solo firmado
Firma → HMAC-SHA256(header+"."+payload, secreto_del_servidor)
alg: HS256 | Simétrico: misma clave para firmar y verificar (solo el servidor la conoce) |
| Envío | Authorization: Bearer <token> — en el header HTTP, NO en la URL |
| Validez | 1 día (campo exp en el payload) |
| Almacenamiento | sessionStorage (se borra al cerrar la pestaña) |
El JWT se envía en el header Authorization, NO como parámetro de URL (?authorization=token). (Pregunta 10a → FALSO)
El JWT está en base64url, no en JSON directo. (Pregunta 10d: "tokens JWT en formato JSON" → FALSO)
¿Cuál afirmación es correcta sobre el backend de la asignatura?
No todo el acceso se puede controlar con roles: p.ej. "un usuario solo puede ver SUS propios pedidos" requiere comprobación programática (no solo hasRole).
Spring Security — configuración
http
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/users/login").permitAll()
.requestMatchers(HttpMethod.POST, "/users/signUp").permitAll()
.requestMatchers(HttpMethod.GET, "/catalog/**").permitAll()
.requestMatchers("/shopping/**").hasRole("USER")
.anyRequest().denyAll() // ← todo lo demás: denegado
);
CORS — por qué es necesario
Frontend: http://localhost:5173 ← origen A
Backend: http://localhost:8080 ← origen B (distinto puerto = distinto origen)
Sin CORS → el navegador bloquea las peticiones fetch del frontend al backend
Con CORS → el backend añade cabeceras: Access-Control-Allow-Origin: *
El mismo origen = mismo protocolo + mismo host + mismo puerto. Si difiere cualquiera → orígenes distintos → Same-origin policy → se necesita CORS. Configurado en SecurityConfig.corsConfigurationSource().
Tema 5 — React: Fundamentos ★ EXAMEN
Componentes y JSX ★ EXAMEN
// Componente = función que devuelve JSX
const Greeting = ({ name }) => {
return (
<div className="container"> {/* ← className, NO class — EXAMEN */}
<h1>{name}</h1> {/* ← llaves para expresiones JS */}
<p>{2 + 2}</p>
</div>
);
};
// Fragment — cuando no hay elemento raíz natural
const Pair = () => (
<>
<span>A</span>
<span>B</span>
</> // no genera ningún elemento HTML
);
Trampas JSX (Pregunta 11):
- Atributos en camelCase:
className(noclass),htmlFor(nofor),onClick,onChange... - Variables JS con llaves
{"{variable}"}, NO con@variable(eso es otro framework) - Los navegadores NO ejecutan JSX directamente — necesita transpilación con Babel/Vite
- Componentes propios en Mayúscula; elementos HTML en minúscula
¿Cuál afirmación es correcta sobre JSX?
Reglas de los Hooks
- Solo en componentes funcionales o custom hooks (no en clases ni funciones normales)
- Solo al nivel raíz del componente — no dentro de if, for, funciones anidadas
- Los custom hooks deben empezar por
use
Props
// Pasar props
<ProductCard product={p} onBuy={handleBuy} />
// Recibir por destructuring
const ProductCard = ({ product, onBuy }) => { ... }
// key obligatoria en listas — usar ID de BD, no índice del array
products.map(p => <ProductCard key={p.id} product={p} />)
Tema 5 — React: useState, listas, Virtual DOM ★ EXAMEN
useState — comportamiento exacto ★ EXAMEN
const App = () => {
const [todos, setTodos] = useState([]);
// ↑ solo se usa en el PRIMER render
// En renders siguientes, todos tiene el valor actual del estado
const handleAddTodo = text => setTodos([{ text }, ...todos]);
// ↑ llamar a setTodos → RE-RENDERIZACIÓN del componente
return (
<div>
<AddTodo onAddTodo={handleAddTodo}/>
<Todos todos={todos}/>
</div>
);
};
3 trampas de useState (Pregunta 6):
useState([])→ el[]inicial solo se usa en el primer render. En renders siguientestodostiene el valor actual, NO siempre[].- Llamar al setter (
setTodos) → provoca re-renderización. ✓ todosestá en estado LOCAL del componente, NO en Redux. Son conceptos distintos.
Componente App con useState([]) y handleAddTodo. ¿Cuál afirmación es correcta?
Inmutabilidad del estado
// ❌ INCORRECTO — muta el array existente
const addTodo = text => {
todos.push({ text }); // Muta! React no detecta cambio (misma referencia)
setTodos(todos);
};
// ✅ CORRECTO — nuevo array
const addTodo = text => setTodos([{ text }, ...todos]);
// ✅ CORRECTO para objetos
setUser({ ...user, name: "nuevo" });
// Por qué importa: useSelector y React usan === para comparar
Listas y prop key
const ProductList = ({ products }) => (
<ul>
{products.map(p => (
<li key={p.id}> {/* ← key obligatorio, único entre siblings */}
{p.name} {/* ← usar ID de BD, no índice del array */}
</li>
))}
</ul>
);
La key debe ser el ID de BD, no el índice del array. Usar el índice causa bugs al reordenar porque React reutiliza elementos DOM equivocados.
Virtual DOM ★ EXAMEN
- setState() → React re-ejecuta la función del componente → genera nuevo árbol VDOM
- Compara nuevo VDOM con el anterior (reconciliation/diffing)
- Calcula el mínimo conjunto de cambios necesarios
- Aplica SOLO esos cambios al DOM real
¿Cuál afirmación sobre renderización es correcta?
Tema 6 — Redux ★ EXAMEN
Conceptos
| Store | Estado global único. configureStore({reducer}) |
| Action | Objeto plano: {type: 'TIPO', ...payload} |
| Action creator | Función que crea y devuelve una action |
| Reducer | Función pura (state, action) => newState. Nunca muta. |
| Selector | Función pura rootState => valor. Recibe el ESTADO RAÍZ. |
| dispatch | Llama al reductor RAÍZ → actualiza store → notifica suscriptores |
Reducer — reglas críticas ★ EXAMEN
const initialState = { messages: [] };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'MARK_MESSAGE_AS_READ':
// ❌ INCORRECTO — muta el array y devuelve el mismo:
// state.messages.find(m => m.id === action.messageId).read = true;
// return state; // ← misma referencia → useSelector NO detecta cambio
// ✅ CORRECTO — nuevo array, nuevo objeto:
return {
...state,
messages: state.messages.map(m =>
m.id === action.messageId ? { ...m, read: true } : m
)
};
default:
return state; // ← SIEMPRE devolver state en default
}
};
Selectores — reciben el ESTADO RAÍZ ★ EXAMEN
// Estado shape con combineReducers:
// { app: {...}, users: {...}, catalog: {...}, messages: {...} }
// ❌ INCORRECTO — asumir que recibe solo la propiedad messages:
export const getNumberOfUnreadMessages = messages =>
messages.filter(m => !m.read).length;
// ✅ CORRECTO — recibe el estado RAÍZ completo:
export const getNumberOfUnreadMessages = state =>
state.messages.filter(m => !m.read).length;
useSelector — cuándo re-renderiza ★ EXAMEN
const UnreadMessagesIcon = () => {
const count = useSelector(selectors.getNumberOfUnreadMessages);
// Re-renderiza SOLO si el valor devuelto por el selector cambia (===)
// Si se borra un mensaje ya leído → messages cambia → pero count no cambia
// → useSelector detecta count === count → NO re-renderiza
return <div>{count}</div>;
};
useSelector compara con === el valor anterior y el nuevo devuelto por el selector. Si messages cambia (nuevo array) pero el número de no leídos es el mismo, devuelve el mismo número → === → NO re-renderiza.
App de email. dispatch(markMessageAsRead). ¿Cuál afirmación es correcta?
combineReducers y flujo completo
const rootReducer = combineReducers({
app: app.reducer,
users: users.reducer,
catalog: catalog.reducer,
shopping: shopping.reducer,
});
// Estado: { app:{...}, users:{...}, catalog:{...}, shopping:{...} }
// Flujo: dispatch(action)
// → rootReducer(state, action)
// → catalog.reducer(state.catalog, action)
// → shopping.reducer(state.shopping, action)
// → nuevo estado en el store
// → useSelector compara (===) → re-renderiza si cambió
async/await — comportamiento en JS ★ EXAMEN
const handleSubmit = async event => {
event.preventDefault();
// [1]
const response = await backend.shoppingService.buy(cartId, address, code);
// ↑ NO bloquea el thread JS
// await suspende ESTA función async y libera el thread para otras tareas
// El thread puede procesar otros eventos mientras espera la respuesta
// Cuando llega la respuesta, la función se reanuda
if (response.ok) { ... }
};
await backend.shoppingService.buy(...). ¿Cuál afirmación es correcta?
Tema 7.1 — PA Shop: Arquitectura global
Estructura de módulos
src/
├── modules/
│ ├── app/ → App.jsx, Header.jsx, Body.jsx
│ ├── users/ → SignUp, Login, autenticación
│ ├── catalog/ → FindProducts, ProductDetails
│ ├── shopping/ → Buy, ShoppingCart, Orders
│ └── common/ → BackLink, Errors (reutilizables)
├── backend/
│ └── appFetch.js → abstracción HTTP + JWT automático
├── store/
│ └── rootReducer.js
└── i18n/messages/ → messages_en.js, messages_es.js
Módulo — estructura interna
// Cada módulo tiene:
// actions.js, actionTypes.js, reducer.js, selectors.js, components/, index.js
// index.js — re-exporta todo
export { default as FindProducts } from './components/FindProducts';
export { default as ProductDetails } from './components/ProductDetails';
import actions from './actions';
import * as selectors from './selectors';
export default { actions, reducer, selectors };
rootReducer y estado global
const rootReducer = combineReducers({
app: app.reducer, // { locale }
users: users.reducer, // { loggedIn, user, jwt }
catalog: catalog.reducer, // { categories, productSearch }
shopping: shopping.reducer, // { cart, lastOrderId }
});
main.jsx — todos los wrappers
root.render(
<React.StrictMode>
<Provider store={store}> {/* Redux */}
<IntlProvider locale={locale} messages={messages}> {/* i18n */}
<BrowserRouter> {/* React Router */}
<App/>
</BrowserRouter>
</IntlProvider>
</Provider>
</React.StrictMode>
);
Body.jsx — routing condicional
const Body = () => {
const loggedIn = useSelector(users.selectors.isLoggedIn);
return (
<Routes>
<Route path="/*" element={<Home/>}/>
<Route path="/catalog/product-details/:id" element={<ProductDetails/>}/>
{loggedIn && <Route path="/shopping/buy" element={<Buy/>}/>}
{!loggedIn && <Route path="/users/login" element={<Login/>}/>}
</Routes>
);
};
appFetch
const appFetch = async (method, path, body) => {
const jwt = sessionStorage.getItem('serviceToken');
const config = {
method,
headers: { 'Content-Type': 'application/json' },
};
if (jwt) config.headers['Authorization'] = `Bearer ${jwt}`;
if (body) config.body = JSON.stringify(body);
const response = await fetch(`http://localhost:8080${path}`, config);
const payload = response.status !== 204 ? await response.json() : null;
return { ok: response.ok, payload }; // ← siempre { ok, payload }
};
Tema 7.2 — PA Shop: Buscar productos
Componentes controlados vs no controlados
| No controlado | Controlado (usado en PA Shop) |
|---|---|
Datos en el DOM → ref | Datos en estado local → useState |
Leer con ref.current.value | value={state} + onChange |
| Difícil validar | React controla el valor en todo momento |
FindProducts
const FindProducts = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const categories = useSelector(catalog.selectors.getCategories);
const [categoryId, setCategoryId] = useState('');
const [keywords, setKeywords] = useState('');
const handleSubmit = async event => {
event.preventDefault();
dispatch(catalog.actions.clearProductSearch());
navigate('/catalog/find-products-result'); // Navega ANTES de la respuesta
const response = await backend.catalogService.findProducts({...});
if (response.ok) {
dispatch(catalog.actions.findProductsCompleted({
criteria, result: response.payload
}));
}
};
return (
<Form onSubmit={handleSubmit}>
<Form.Select value={categoryId} onChange={e => setCategoryId(e.target.value)}>
<option value="">-- Todas --</option>
{categories?.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</Form.Select>
<Form.Control value={keywords} onChange={e => setKeywords(e.target.value)}/>
<Button type="submit">Buscar</Button>
</Form>
);
};
Reducer del módulo catalog
const initialState = { categories: null, productSearch: null };
const productSearch = (state = initialState.productSearch, action) => {
switch (action.type) {
case actionTypes.FIND_PRODUCTS_COMPLETED:
return action.productSearch;
case actionTypes.CLEAR_PRODUCT_SEARCH:
return initialState.productSearch; // null
default:
return state;
}
};
const reducer = combineReducers({ categories, productSearch });
FindProductsResult — paginación
const FindProductsResult = () => {
const productSearch = useSelector(catalog.selectors.getProductSearch);
if (!productSearch) return null;
const { criteria, result: { items, existMoreItems } } = productSearch;
const loadMore = async () => {
const newCriteria = { ...criteria, page: criteria.page + 1 };
const response = await backend.catalogService.findProducts(newCriteria);
if (response.ok) {
dispatch(catalog.actions.findProductsCompleted({
criteria: newCriteria, result: response.payload
}));
}
};
return (
<>
<Products products={items}/>
{existMoreItems && <Button onClick={loadMore}>Cargar más</Button>}
</>
);
};
Tema 7.3 — PA Shop: useEffect ★ EXAMEN
ProductDetails — estado local + useEffect
Los datos del detalle de producto se guardan en estado LOCAL (useState), no en Redux, porque solo los necesita este componente.
const ProductDetails = () => {
const loggedIn = useSelector(users.selectors.isLoggedIn);
const [product, setProduct] = useState(null); // ← estado LOCAL
const { id } = useParams();
const productId = Number(id);
useEffect(() => {
const findProductById = async productId => {
if (!Number.isNaN(productId)) {
const response = await backend.catalogService.findProductById(productId);
if (response.ok) {
setProduct(response.payload);
}
}
};
findProductById(productId);
}, [productId]); // ← dependencias del efecto
if (!product) return null; // Primer render: product=null → no muestra nada
return (
<>
<BackLink/>
<h2>{product.name}</h2>
{loggedIn && <AddToShoppingCart productId={productId}/>}
</>
);
};
useEffect — semántica completa ★ EXAMEN
useEffect(efecto, [dependencias])
Dependencias │ Cuándo ejecuta {clean-up} + {efecto}
────────────────────────┼───────────────────────────────────────
Sin segundo argumento │ Tras CADA render
[] (array vacío) │ Solo tras el PRIMER render (mount)
[dep1, dep2] │ Tras mount + cuando dep1 o dep2 cambian
Orden de ejecución:
1. Primer render → ejecuta {efecto}
2. Re-render, dep ≠ → ejecuta {clean-up previo} + {efecto}
3. Re-render, dep == → NO ejecuta nada
4. Unmount → ejecuta {clean-up}
Flujo de ProductDetails
[1] Primer render → product=null → devuelve null (no muestra nada)
[2] Tras ese render → se ejecuta el efecto → fetch al backend
[3] Llega respuesta → setProduct(response.payload) → RE-RENDER
[4] Segundo render → product tiene datos → muestra la vista
[5] productId no cambió → efecto NO se vuelve a ejecutar
Con StrictMode en desarrollo: el efecto se ejecuta dos veces (mount→unmount→mount). En ProductDetails esto envía 2 peticiones GET. Solo ocurre en desarrollo, no en producción.
Clean-up function
useEffect(() => {
const subscription = eventBus.subscribe('topic', handleEvent);
return () => {
subscription.unsubscribe(); // Se ejecuta antes del próximo efecto o al unmount
};
}, []); // Solo se suscribe al montar y desuscribe al desmontar
Tema 7.4 — PA Shop: Comprar y formularios ★ EXAMEN
BuyForm — estructura completa
const BuyForm = ({ shoppingCartId }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
// Estado LOCAL del formulario (NO en Redux):
const [postalAddress, setPostalAddress] = useState('');
const [postalCode, setPostalCode] = useState('');
const [backendErrors, setBackendErrors] = useState(null);
const [formValidated, setFormValidated] = useState(false);
let form; // referencia al nodo DOM del formulario
const handleSubmit = async event => {
event.preventDefault();
if (form.checkValidity()) { // valida HTML5 programáticamente
const response = await backend.shoppingService.buy(
shoppingCartId, postalAddress, postalCode
);
if (response.ok) {
dispatch(actions.buyCompleted(response.payload));
navigate('/shopping/purchase-completed');
} else {
setBackendErrors(response.payload); // errores → estado LOCAL
}
} else {
setBackendErrors(null);
setFormValidated(true); // activa visualización de errores cliente
}
};
return (
<div>
<Errors errors={backendErrors} onClose={() => setBackendErrors(null)}/>
<Form ref={node => form = node}
validated={formValidated}
noValidate {/* deshabilita validación automática del browser */}
onSubmit={e => handleSubmit(e)}>
<Form.Group controlId="postalAddress">
<Form.Control
type="text"
value={postalAddress}
onChange={e => setPostalAddress(e.target.value)}
required {/* restricción HTML5 */}
/>
<Form.Control.Feedback type="invalid">
<FormattedMessage id="...required"/>
</Form.Control.Feedback>
</Form.Group>
<Button type="submit">Comprar</Button>
</Form>
</div>
);
};
Validaciones HTML5 — dos enfoques ★ EXAMEN
| Enfoque declarativo | Enfoque programático (PA Shop) |
|---|---|
| El browser ejecuta automáticamente al submit | noValidate desactiva ejecución automática |
| No hace falta código JS para lanzarlas | Developer llama form.checkValidity() |
| Mensajes en idioma del navegador | Mensajes personalizables con React Intl |
| Look-and-feel no personalizable | Look-and-feel con React Bootstrap |
3 trampas de formularios (Pregunta 12):
- Los errores del backend se guardan en estado LOCAL (
useState), NO en Redux. - Con
noValidate(enfoque programático), las restricciones HTML5 NO se ejecutan automáticamente al pulsar submit — hay que llamar aform.checkValidity(). - Aunque el backend valide, SÍ se deben especificar restricciones HTML5 en el frontend (para UX: feedback inmediato al usuario).
¿Cuál afirmación es correcta sobre el enfoque de desarrollo del frontend?
El backend siempre debe validar todo (incluido lo que valida el frontend), por seguridad. Las validaciones del cliente son UX, no seguridad.
Tema 7.5 — PA Shop: CORS, recargas, sessionStorage
Recargas en Vite
| Modifica un .jsx | Modifica un .js |
|---|---|
| Solo ese componente recarga (hot reload) | Todo el frontend recarga (full reload) |
| En muchos casos, el estado local (useState) se conserva | Todo el estado se pierde (Redux, useState, todo) |
Para evitar re-autenticarse tras recargar un .js: al montar App se hace POST /users/loginFromServiceToken con el JWT de sessionStorage → el backend devuelve perfil de usuario y carrito.
JWT en sessionStorage
sessionStorage.setItem('serviceToken', jwt); // Al autenticarse
sessionStorage.getItem('serviceToken'); // appFetch lo lee automáticamente
sessionStorage.removeItem('serviceToken'); // Al cerrar sesión
// localStorage: persiste al cerrar el navegador
// sessionStorage: se elimina al cerrar la pestaña ← PA Shop usa este
Same-origin policy y CORS
Mismo origen = mismo protocolo + mismo host + mismo puerto
http://localhost:5173 ≠ http://localhost:8080
↑ puerto distinto = origen distinto
Sin CORS → navegador bloquea las peticiones fetch (Same-origin policy)
Con CORS → backend añade cabeceras en respuestas:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
CORS se configura en el backend (SecurityConfig.corsConfigurationSource()). Es el backend quien autoriza el acceso cross-origin, no el frontend.
Tema 8 — Microservicios, Docker, Mobile
Microservicios
| Core µS | BD propia. Casos de uso finales. Ej: UserService, CatalogService. |
| Composition µS | Coordinan varios core µS. Sin BD propia. |
| Gateway | Fachada. Punto de entrada único para los clientes. |
Docker
docker build -t my-backend . # Crear imagen (código + dependencias + JRE)
docker run -p 8080:8080 my-backend # Ejecutar contenedor
docker-compose up # Levantar todo el stack
Cloud
| Pública | AWS, Azure, Google Cloud. Infraestructura compartida. |
| Privada | CloudFoundry, OpenShift. En servidores propios. |
React Native
import { View, Text, Button } from 'react-native';
// View ≈ div, Text ≈ p/span
// NO hay HTML, NO hay DOM, NO hay CSS estándar
// NO usa un browser embebido → app NATIVA en iOS/Android
Apps híbridas
| Capacitor/Electron | SPA dentro de un browser embebido en la app. |
| vs React Native | Híbridas más fáciles si ya tienes SPA. React Native tiene mejor rendimiento y look nativo. |
Resumen Examen — Todas las trampas
JPA — Relaciones
| @ManyToOne(mappedBy=...) | INCORRECTO — mappedBy NO existe en @ManyToOne |
| @ManyToOne + @JoinColumn | CORRECTO — va en el lado N (owning, tiene la FK) |
| @JoinColumn en @OneToMany | INCORRECTO — @JoinColumn nunca en el lado inverso |
| @OneToMany(mappedBy=...) | CORRECTO — mappedBy en el lado 1 (inverso) |
JPA — LAZY
| proxy.getId() | NO lanza query (ID disponible en el proxy) |
| proxy.getName() | SÍ lanza query (primera propiedad no-ID) |
| colección = order.getOrderItems() | NO lanza query (devuelve proxy de colección) |
| primer for(item : colección) | SÍ lanza query (primera iteración) |
@Transactional
| Checked exception → ROLLBACK | FALSO → Checked exception → COMMIT |
| RuntimeException / Error → ROLLBACK | CORRECTO |
| readOnly=true → no corre en transacción | FALSO — sí corre en transacción (solo lectura) |
| Propagación REQUIRED (default) → join | CORRECTO — se une a la transacción existente |
Tests
| @ActiveProfiles carga SOLO application-test.yml | FALSO — carga ambos yml; test sobreescribe al principal |
| Hay que borrar datos manualmente en tests @Transactional | FALSO — rollback automático |
| Necesitas equals/hashCode para assertEquals | FALSO — misma sesión Hibernate = mismo objeto Java |
| maximum-pool-size: 1 en tests | CORRECTO — evita deadlocks |
REST
| @Service en lugar de @Autowired | FALSO — son distintos: @Service en la clase, @Autowired en el punto de inyección |
| Sin @RequestBody Spring lee el body JSON | FALSO — @RequestBody es obligatorio para deserializar el body |
| JWT en parámetro URL ?authorization=token | FALSO — va en header Authorization: Bearer token |
| Fat JAR necesita servidor de aplicaciones externo | FALSO — incluye Tomcat embebido, solo java -jar |
| JWT en formato JSON puro | FALSO — en base64url |
React / JSX
| className | CORRECTO en JSX |
| class | INCORRECTO en JSX |
| @variable | INCORRECTO — se usan llaves: {variable} |
| El browser ejecuta JSX directamente | FALSO — necesita Babel/Vite para transpilar |
| useState([]) → todos siempre vale [] | FALSO — el [] solo es el valor inicial (primer render) |
| todos en useState = estado Redux | FALSO — es estado LOCAL del componente |
Redux
| Selector recibe solo su slice del estado | FALSO — recibe el estado RAÍZ completo |
| Reducer muta y devuelve el mismo objeto | FALSO — debe devolver un objeto NUEVO |
| dispatch → llama al reductor RAÍZ | CORRECTO |
| useSelector re-renderiza si array cambia aunque el valor no cambie | FALSO — compara con === el valor devuelto por el selector |
| await bloquea el thread JS | FALSO — suspende la función async pero libera el thread |
Formularios (T7)
| Errores del backend → estado Redux | FALSO — estado LOCAL del componente (useState) |
| Con noValidate, restricciones HTML5 se ejecutan solas | FALSO — hay que llamar form.checkValidity() |
| Si el backend valida, no hace falta restricciones en el frontend | FALSO — el frontend también valida (UX) |