Preguntas de entrevista Full-Stack con código y respuestas (45+)
Si estás preparando una entrevista Full-Stack, sabés que el panel puede preguntarte desde CSS hasta arquitectura de microservicios en la misma sesión. Esta guía cubre las 45+ preguntas más frecuentes — con código real, no pseudocódigo — para que no te agarren desprevenido.
Las preguntas están organizadas por área: Frontend, Backend, Bases de datos, Arquitectura y Comportamiento. Usá el índice para saltar directo a lo que más te preocupa.
Índice
- [Frontend (preguntas 1–12)](#frontend)
- [Backend con Node.js (preguntas 13–22)](#backend)
- [Bases de datos (preguntas 23–30)](#bases-de-datos)
- [Arquitectura y sistema (preguntas 31–38)](#arquitectura)
- [TypeScript y herramientas (preguntas 39–42)](#typescript)
- [Comportamiento y diseño de sistemas (preguntas 43–47)](#comportamiento)
Frontend {#frontend}
1. ¿Cuál es la diferencia entre `==` y `===` en JavaScript?
== hace coerción de tipos antes de comparar. === compara valor Y tipo sin ninguna conversión.
console.log(0 == false); // true — false se convierte a 0
console.log(0 === false); // false — number vs boolean, distintos tipos
console.log('' == false); // true — ambos se convierten a 0
console.log('' === false); // false
console.log(null == undefined); // true — caso especial del lenguaje
console.log(null === undefined); // falseRegla práctica: usá siempre ===. La única excepción aceptada es x == null cuando querés atrapar tanto null como undefined de una vez.
2. Explicá el event loop de JavaScript con un ejemplo
JavaScript es single-threaded. El event loop es el mecanismo que le permite manejar operaciones asíncronas sin bloquear el hilo principal.
console.log('1 - sync');
setTimeout(() => {
console.log('2 - macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3 - microtask (Promise)');
});
console.log('4 - sync');
// Output:
// 1 - sync
// 4 - sync
// 3 - microtask (Promise) ← microtasks van ANTES que macrotasks
// 2 - macrotask (setTimeout)El orden: stack de ejecución → cola de microtasks (Promises, queueMicrotask) → cola de macrotasks (setTimeout, setInterval, I/O).
3. ¿Qué es el closure y para qué se usa?
Un closure es una función que "recuerda" el scope donde fue creada, incluso después de que ese scope ya no está activo.
function makeCounter(initialValue = 0) {
let count = initialValue; // esta variable vive en el closure
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.value()); // 11
// count no es accesible desde afuera — encapsulación real
console.log(counter.count); // undefinedUsos comunes: encapsulación de estado privado, funciones de fábrica, memoización, event handlers con contexto capturado.
4. ¿Cómo funciona `this` en JavaScript? ¿Y en arrow functions?
this es dinámico — su valor depende de *cómo* se llama la función, no de dónde se define.
const obj = {
name: 'InterviewHack',
// Función regular: this = el objeto que la llama
greetRegular: function () {
console.log(`Hola desde ${this.name}`);
},
// Arrow function: this = el this del scope donde fue DEFINIDA
greetArrow: () => {
console.log(`Hola desde ${this?.name}`); // this es el scope global aquí
},
};
obj.greetRegular(); // "Hola desde InterviewHack"
obj.greetArrow(); // "Hola desde undefined"
// Problema clásico en callbacks
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// MAL: this.seconds es undefined dentro de la función regular
// setInterval(function() { this.seconds++; }, 1000);
// BIEN: arrow function hereda el this del método start
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}5. ¿Cuál es la diferencia entre `var`, `let` y `const`?
| | var | let | const |
|---|---|---|---|
| Scope | función | bloque | bloque |
| Hoisting | sí (undefined) | sí (TDZ) | sí (TDZ) |
| Re-asignable | sí | sí | no |
| Re-declarable | sí | no | no |
// var tiene scope de función, no de bloque
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 — i "escapó" del for
// let tiene scope de bloque
for (let j = 0; j < 3; j++) {}
// console.log(j); // ReferenceError
// const no significa inmutable para objetos
const user = { name: 'Santiago' };
user.name = 'Carlos'; // OK — mutás la propiedad, no la referencia
// user = {}; // TypeError — no podés reasignar la referenciaRegla: usá const por default. let solo cuando necesitás reasignar. Nunca var en código nuevo.
6. ¿Cómo funciona el Virtual DOM en React?
El Virtual DOM es una representación en memoria del DOM real. React lo usa para minimizar las operaciones costosas de actualización del DOM.
// Cuando el estado cambia, React:
// 1. Crea un nuevo Virtual DOM tree
// 2. Lo compara con el anterior (diffing/reconciliation)
// 3. Calcula el mínimo de cambios necesarios
// 4. Aplica solo esos cambios al DOM real (patching)
function UserCard({ name, email }) {
return (
<div className="card">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
// Si solo cambia `email`, React solo actualiza el <p>
// No re-renderiza el <h2> ni el <div> padreKey prop: le dice a React cómo identificar elementos en listas para el diffing.
// MAL: React no puede rastrear qué elemento cambió
{users.map((u, index) => <UserCard key={index} {...u} />)}
// BIEN: key estable y única
{users.map(u => <UserCard key={u.id} {...u} />)}7. ¿Qué son los React Hooks y cuáles son los más importantes?
Los Hooks permiten usar estado y otras features de React en componentes funcionales.
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const abortRef = useRef(null);
// useEffect: side effects (fetch, subscriptions, timers)
useEffect(() => {
if (!query) return;
abortRef.current?.abort();
abortRef.current = new AbortController();
setLoading(true);
fetch(`/api/search?q=${query}`, { signal: abortRef.current.signal })
.then(r => r.json())
.then(data => {
setResults(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') setLoading(false);
});
return () => abortRef.current?.abort(); // cleanup
}, [query]);
// useMemo: valor derivado costoso de calcular
const sortedResults = useMemo(
() => [...results].sort((a, b) => b.score - a.score),
[results]
);
// useCallback: función estable para pasar como prop
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []); // sin dependencias = misma referencia siempre
return loading ? <Spinner /> : <List items={sortedResults} onClick={handleClick} />;
}8. ¿Qué es CSS-in-JS y cuándo usarlo?
CSS-in-JS es escribir estilos directamente en JavaScript/TypeScript. Resuelve el problema de scope global del CSS tradicional.
// Con styled-components
import styled from 'styled-components';
const Button = styled.button<{ variant: 'primary' | 'secondary' }>`
padding: 8px 16px;
border-radius: 6px;
background: ${({ variant }) => variant === 'primary' ? '#0070f3' : 'transparent'};
color: ${({ variant }) => variant === 'primary' ? 'white' : '#0070f3'};
border: 2px solid #0070f3;
&:hover {
opacity: 0.85;
}
`;
// Con Tailwind (utility-first, alternativa popular)
function ButtonTW({ variant, children }) {
const base = 'px-4 py-2 rounded-md border-2 border-blue-500 hover:opacity-85';
const variants = {
primary: 'bg-blue-500 text-white',
secondary: 'bg-transparent text-blue-500',
};
return <button className={`${base} ${variants[variant]}`}>{children}</button>;
}Cuándo CSS-in-JS: design systems con mucha variación dinámica, cuando el styling depende de lógica compleja de negocio.
Cuándo Tailwind/CSS Modules: equipos grandes, performance crítica (zero-runtime), SSR sin hydration issues.
9. Explicá `Promise.all`, `Promise.race`, `Promise.allSettled` y `Promise.any`
const fetchUser = id => fetch(`/api/users/${id}`).then(r => r.json());
const fetchPosts = id => fetch(`/api/posts?userId=${id}`).then(r => r.json());
// Promise.all — espera TODAS; si una falla, falla todo
const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]);
// Útil cuando necesitás todos los datos y son independientes entre sí
// Promise.allSettled — espera TODAS sin importar si fallan
const results = await Promise.allSettled([fetchUser(1), fetchPosts(1)]);
results.forEach(result => {
if (result.status === 'fulfilled') console.log(result.value);
else console.error(result.reason);
});
// Útil para dashboards donde querés mostrar lo que salió bien
// Promise.race — resuelve/rechaza con la PRIMERA que termine
const data = await Promise.race([
fetchUser(1),
new Promise((_, reject) => setTimeout(() => reject('timeout'), 3000))
]);
// Útil para timeouts
// Promise.any — resuelve con la PRIMERA que tenga éxito (ignora rechazos)
const fastestMirror = await Promise.any([
fetch('https://mirror1.example.com/file'),
fetch('https://mirror2.example.com/file'),
fetch('https://mirror3.example.com/file'),
]);
// Útil para redundancia / fallbacks10. ¿Cómo optimizás el rendimiento de una aplicación React?
import React, { memo, lazy, Suspense } from 'react';
// 1. React.memo — evita re-render si las props no cambiaron
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* renderizado costoso */}</div>;
});
// 2. Code splitting con lazy
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
);
}
// 3. Virtualización para listas largas
import { FixedSizeList } from 'react-window';
function BigList({ items }) {
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
// 4. Evitar objetos/funciones inline en JSX
// MAL: nueva referencia en cada render → siempre re-renderiza hijos
<Child config={{ size: 'lg' }} onClick={() => doSomething()} />
// BIEN: referencias estables
const CONFIG = { size: 'lg' };
const handleClick = useCallback(() => doSomething(), []);
<Child config={CONFIG} onClick={handleClick} />11. ¿Qué es el Box Model en CSS?
.box {
/* Orden: content → padding → border → margin */
width: 200px; /* ancho del content area */
padding: 16px; /* espacio interno */
border: 2px solid; /* borde */
margin: 24px; /* espacio externo */
/*
Sin box-sizing: ancho total = 200 + 32 + 4 = 236px (confuso)
Con border-box: ancho total = 200px (padding y border van ADENTRO)
*/
box-sizing: border-box; /* mejor práctica — siempre en el reset */
}
/* Reset universal recomendado */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}12. ¿Cómo funciona Flexbox vs Grid?
/* FLEXBOX — una dimensión (fila O columna) */
.nav {
display: flex;
align-items: center; /* eje secundario (cross axis) */
justify-content: space-between; /* eje principal (main axis) */
gap: 16px;
}
/* GRID — dos dimensiones (filas Y columnas) */
.dashboard {
display: grid;
grid-template-columns: 250px 1fr; /* sidebar fijo + contenido flexible */
grid-template-rows: 64px 1fr; /* header + main */
min-height: 100vh;
}
.sidebar { grid-row: 1 / -1; } /* abarca todas las filas */
/* Grid responsive sin media queries */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}Regla: Flexbox para componentes lineales (navbars, botones, listas). Grid para layouts de página y grillas bidimensionales.
Backend con Node.js {#backend}
13. ¿Cómo manejás errores en Express de forma centralizada?
// middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
const status = err.statusCode || 500;
const message = err.message || 'Error interno del servidor';
// Loggear en producción (Sentry, Datadog, etc.)
if (status >= 500) {
console.error('[Error]', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
}
res.status(status).json({
error: {
message,
code: err.code || 'INTERNAL_ERROR',
// Solo exponer stack en desarrollo
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
});
}
// Clase base para errores de negocio
export class AppError extends Error {
constructor(message, statusCode = 500, code = 'APP_ERROR') {
super(message);
this.statusCode = statusCode;
this.code = code;
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
export class ValidationError extends AppError {
constructor(message) {
super(message, 400, 'VALIDATION_ERROR');
}
}
// En el router
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
} catch (err) {
next(err); // pasa al middleware de error
}
});
// Al final de todos los routes
app.use(errorHandler);14. ¿Cómo implementás autenticación JWT?
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// Generar tokens
function generateTokens(userId) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' } // corto — si se compromete, expira rápido
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Login
async function login(req, res, next) {
try {
const { email, password } = req.body;
const user = await db.users.findByEmail(email);
if (!user) throw new AppError('Credenciales inválidas', 401, 'INVALID_CREDENTIALS');
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) throw new AppError('Credenciales inválidas', 401, 'INVALID_CREDENTIALS');
const tokens = generateTokens(user.id);
// Guardar refresh token en DB para poder revocarlo
await db.refreshTokens.create({ userId: user.id, token: tokens.refreshToken });
// Refresh token en httpOnly cookie (no accesible desde JS)
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: tokens.accessToken });
} catch (err) {
next(err);
}
}
// Middleware de autenticación
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return next(new AppError('Token requerido', 401, 'UNAUTHORIZED'));
}
try {
const token = authHeader.slice(7);
const payload = jwt.verify(token, ACCESS_SECRET);
req.userId = payload.sub;
next();
} catch {
next(new AppError('Token inválido o expirado', 401, 'INVALID_TOKEN'));
}
}15. Explicá el patrón Repository con un ejemplo real
// Separás la lógica de acceso a datos del negocio
// interfaces/UserRepository.ts
export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
create(data: CreateUserDto): Promise<User>;
update(id: string, data: UpdateUserDto): Promise<User>;
delete(id: string): Promise<void>;
}
// repositories/SupabaseUserRepository.ts
export class SupabaseUserRepository implements UserRepository {
constructor(private readonly supabase: SupabaseClient) {}
async findById(id: string) {
const { data, error } = await this.supabase
.from('users')
.select('*')
.eq('id', id)
.single();
if (error) throw new AppError(error.message, 500);
return data;
}
async findByEmail(email: string) {
const { data } = await this.supabase
.from('users')
.select('*')
.eq('email', email)
.single();
return data;
}
async create(dto: CreateUserDto) {
const { data, error } = await this.supabase
.from('users')
.insert({ ...dto, user_id: dto.id }) // RLS: user_id explícito
.select()
.single();
if (error) throw new AppError(error.message, 500);
return data;
}
// ...
}
// services/UserService.ts — lógica de negocio, sin saber qué DB usa
export class UserService {
constructor(private readonly users: UserRepository) {}
async getProfile(id: string) {
const user = await this.users.findById(id);
if (!user) throw new NotFoundError('User');
return user;
}
}16. ¿Qué es middleware en Express y cómo funciona la cadena?
// Un middleware es (req, res, next) => void
// La cadena se ejecuta en orden hasta que alguien llama res.send() o res.end()
import express from 'express';
const app = express();
// 1. Middleware global (aplica a todas las rutas)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 2. Middleware de logging
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms`);
});
next(); // IMPORTANTE: si no llamás next(), la cadena se corta
});
// 3. Middleware de autenticación selectivo
const requireAuth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
// No llama next() — corta la cadena con respuesta
}
next();
};
// 4. Aplicar solo a rutas específicas
app.get('/public', (req, res) => res.json({ ok: true }));
app.get('/private', requireAuth, (req, res) => res.json({ secret: true }));
// 5. Múltiples middlewares en una ruta
app.post(
'/admin/users',
requireAuth,
requireRole('admin'),
validateBody(CreateUserSchema),
createUserHandler
);17. ¿Cómo diseñás una API REST siguiendo buenas prácticas?
Recursos como sustantivos, métodos HTTP como verbos:
GET /users → listar usuarios
GET /users/:id → obtener usuario
POST /users → crear usuario
PUT /users/:id → reemplazar usuario completo
PATCH /users/:id → actualizar parcialmente
DELETE /users/:id → eliminar
Relaciones:
GET /users/:id/posts → posts de un usuario
POST /users/:id/posts → crear post de un usuario
Versioning:
GET /v1/users
GET /v2/users
Query params para filtros/paginación/orden:
GET /users?role=admin&status=active&page=2&limit=20&sort=createdAt:desc// Respuestas consistentes
// Éxito
{ "data": { "id": "123", "name": "Santiago" }, "meta": { "requestId": "abc" } }
// Error
{ "error": { "code": "VALIDATION_ERROR", "message": "Email inválido", "fields": { "email": "Formato incorrecto" } } }
// Paginación
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8
}
}18. ¿Cómo implementás rate limiting?
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
// Rate limiting general
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
message: { error: { code: 'RATE_LIMIT', message: 'Demasiadas solicitudes' } },
});
// Más estricto para auth endpoints (prevenir brute force)
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 10,
message: { error: { code: 'TOO_MANY_ATTEMPTS', message: 'Cuenta temporalmente bloqueada' } },
});
app.use('/api/', limiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/forgot-password', authLimiter);19. ¿Qué es async/await y cómo manejás errores con ello?
// async/await es azúcar sintáctica sobre Promises
// Pattern 1: try/catch (el más común)
async function fetchUserData(userId) {
try {
const user = await db.users.findById(userId);
const posts = await db.posts.findByUserId(userId); // secuencial
return { user, posts };
} catch (err) {
if (err.code === 'NOT_FOUND') throw new NotFoundError('User');
throw err;
}
}
// Pattern 2: paralelo cuando las operaciones son independientes
async function fetchDashboard(userId) {
const [user, posts, stats] = await Promise.all([
db.users.findById(userId),
db.posts.findByUserId(userId),
db.stats.getForUser(userId),
]);
return { user, posts, stats };
}
// Pattern 3: wrapper para evitar try/catch repetitivo
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
const [err, user] = await to(db.users.findById(userId));
if (err) return next(err);
res.json(user);20. ¿Cómo implementás caché con Redis?
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// Patrón Cache-Aside (el más usado)
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. Intentar desde caché
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 2. Cache miss → ir a la DB
const user = await db.users.findById(userId);
if (!user) return null;
// 3. Guardar en caché con TTL
await redis.setEx(cacheKey, 300, JSON.stringify(user)); // 5 minutos
return user;
}
// Invalidar caché al actualizar
async function updateUser(userId, data) {
const user = await db.users.update(userId, data);
await redis.del(`user:${userId}`); // invalidar
return user;
}
// Rate limiting con Redis (manual, más control)
async function checkRateLimit(key, max, windowSeconds) {
const current = await redis.incr(key);
if (current === 1) await redis.expire(key, windowSeconds);
return current <= max;
}21. ¿Qué son los WebSockets y cuándo usarlos vs REST?
// WebSockets: conexión persistente bidireccional
// Usar cuando necesitás push del servidor (chat, notificaciones en tiempo real, colaboración)
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const rooms = new Map(); // roomId → Set<WebSocket>
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
switch (msg.type) {
case 'join-room':
if (!rooms.has(msg.roomId)) rooms.set(msg.roomId, new Set());
rooms.get(msg.roomId).add(ws);
ws.roomId = msg.roomId;
break;
case 'message':
// Broadcast a todos en el room excepto al emisor
rooms.get(ws.roomId)?.forEach(client => {
if (client !== ws && client.readyState === ws.OPEN) {
client.send(JSON.stringify({ type: 'message', data: msg.data }));
}
});
break;
}
});
ws.on('close', () => {
rooms.get(ws.roomId)?.delete(ws);
});
});
/*
REST cuando: operaciones CRUD, datos que el cliente pide explícitamente
WebSockets cuando: chat, live cursors, notificaciones push, juegos, trading en vivo
SSE (Server-Sent Events) cuando: solo necesitás push unidireccional (feeds, updates de progreso)
*/22. ¿Cómo manejás variables de entorno y configuración?
// config/env.ts — validar al arrancar, no al usar
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_ACCESS_SECRET: z.string().min(32),
JWT_REFRESH_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional(),
SENTRY_DSN: z.string().url().optional(),
});
function validateEnv() {
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error('Variables de entorno inválidas:');
console.error(result.error.flatten().fieldErrors);
process.exit(1); // falla rápido, no en el primer request
}
return result.data;
}
export const env = validateEnv();
// Uso
import { env } from './config/env';
const server = app.listen(env.PORT);Bases de datos {#bases-de-datos}
23. ¿Cuál es la diferencia entre SQL y NoSQL?
| | SQL (PostgreSQL, MySQL) | NoSQL (MongoDB, DynamoDB) |
|---|---|---|
| Estructura | Tablas con schema fijo | Documentos/clave-valor/grafos flexibles |
| Relaciones | JOINs, FK, integridad referencial | Desnormalización, referencias manuales |
| Transacciones | ACID completo | Eventual consistency (depende del motor) |
| Escalado | Vertical (principalmente) | Horizontal nativo |
| Cuándo | Datos estructurados, relaciones complejas, finanzas | Catálogos, logs, datos variables por documento |
-- SQL: estructura rígida pero poderosa para relaciones
SELECT
u.name,
COUNT(o.id) AS order_count,
SUM(o.total) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0
ORDER BY total_spent DESC;// NoSQL (MongoDB): flexible, bueno para datos jerárquicos
db.products.find({
'variants.size': 'XL',
'reviews.rating': { $gte: 4 },
$text: { $search: 'running shoes' }
}).sort({ 'stats.sales': -1 }).limit(20);24. ¿Qué son los índices y cuándo crearlos?
-- Sin índice: table scan completo (O(n))
-- Con índice: búsqueda en árbol B-tree (O(log n))
-- Índice simple — bueno para búsquedas por email
CREATE INDEX idx_users_email ON users(email);
-- Índice compuesto — el orden importa (leftmost prefix rule)
-- Sirve para: (user_id), (user_id, status), (user_id, status, created_at)
-- No sirve para: solo (status) o solo (created_at)
CREATE INDEX idx_orders_user_status_date ON orders(user_id, status, created_at);
-- Índice parcial — solo indexa filas que cumplen una condición
-- Más pequeño y eficiente si la condición filtra muchas filas
CREATE INDEX idx_orders_pending ON orders(user_id, created_at)
WHERE status = 'pending';
-- Ver si una query usa el índice
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
/*
Cuándo crear índices:
+ Columnas en WHERE, JOIN ON, ORDER BY frecuentes
+ Foreign keys (siempre)
+ Columnas de alta cardinalidad (email, id)
Cuándo NO crear índices:
- Tablas pequeñas (<10k filas)
- Columnas con pocos valores distintos (boolean, enum de 2 valores)
- Columnas que se escriben mucho (el índice también se actualiza)
*/25. ¿Qué son las transacciones y las propiedades ACID?
// ACID: Atomicity, Consistency, Isolation, Durability
// Ejemplo con PostgreSQL via node-postgres
import { Pool } from 'pg';
const pool = new Pool();
async function transferMoney(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Ambas operaciones son parte de la misma transacción
const { rows: [from] } = await client.query(
'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE', // lock
[fromId]
);
if (from.balance < amount) {
throw new Error('Saldo insuficiente');
}
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
// Atomicidad: ambas o ninguna
// Consistencia: el balance total no cambia
// Aislamiento: otros reads no ven estados intermedios
// Durabilidad: si el servidor cae después del COMMIT, los datos persisten
} catch (err) {
await client.query('ROLLBACK'); // deshace TODO si algo falla
throw err;
} finally {
client.release();
}
}26. ¿Cómo evitás SQL Injection?
// MAL: concatenación directa de strings — nunca hacer esto
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Si userId = "1 OR 1=1 --" → devuelve TODOS los usuarios
// BIEN: queries parametrizadas (los parámetros se escapan automáticamente)
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId] // el driver sanitiza esto
);
// Con ORMs (Prisma, Sequelize) — seguro por default
const user = await prisma.user.findUnique({ where: { id: userId } });
// Raw queries con ORM — igual de seguro con template tags
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
// Nunca interpolar directamente en raw queries
// MAL:
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);27. ¿Qué es N+1 query problem y cómo lo resolvés?
// Problema: 1 query para la lista + N queries para cada elemento
// Con Prisma — lazy loading naive:
const posts = await prisma.post.findMany(); // 1 query
for (const post of posts) {
// N queries — una por cada post
const author = await prisma.user.findUnique({ where: { id: post.authorId } });
console.log(post.title, author.name);
}
// Solución 1: Eager loading con include
const posts = await prisma.post.findMany({
include: { author: true } // 1 query con JOIN
});
// Solución 2: DataLoader (batching para GraphQL)
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids) => {
// Se llama UNA vez con todos los ids acumulados
const users = await prisma.user.findMany({
where: { id: { in: ids } }
});
// Devolver en el mismo orden que los ids
return ids.map(id => users.find(u => u.id === id));
});
// Ahora aunque llames userLoader.load() 100 veces,
// se hace 1 sola query con WHERE id IN (...)
const author = await userLoader.load(post.authorId);28. ¿Cómo diseñás el schema de una base de datos?
-- Principios: normalización, integridad referencial, naming consistente
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
status TEXT NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índices para las FK y queries comunes
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_posts_status_published ON posts(status, published_at)
WHERE status = 'published';
-- Trigger para updated_at automático
CREATE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();29. ¿Qué es la normalización y cuándo desnormalizás?
1NF: cada celda tiene un valor atómico (no arrays, no múltiples valores)
2NF: todos los atributos dependen de la PK completa (no de parte de ella)
3NF: no hay dependencias transitivas (A→B→C: sacar C a su propia tabla)
Ejemplo de desnormalización intencional:
-- Normalizado: para el historial de precios necesitás el precio actual del producto
SELECT oi.quantity, p.price FROM order_items oi JOIN products p ON p.id = oi.product_id
-- PROBLEMA: si el precio del producto cambia, el historial cambia también
-- Desnormalizado: guardar el precio en el momento de la compra
ALTER TABLE order_items ADD COLUMN unit_price DECIMAL(10,2) NOT NULL;
-- Ahora el historial es inmutable e independiente del precio actual
Cuándo desnormalizar:
+ Datos históricos que no deben cambiar (precios, snapshots)
+ Performance crítica con JOINs muy costosos
+ Read-heavy con writes raros (materializar un COUNT o SUM)
Cuándo NO desnormalizar:
- Por default
- Antes de medir que hay un problema real30. ¿Cómo implementás soft delete?
-- Soft delete: marcar como eliminado en lugar de borrar
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ;
-- Con RLS en Supabase para que solo el dueño lo vea
-- o con un view que filtra los eliminados
CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;
-- Índice parcial — no indexar los eliminados (son basura para queries normales)
CREATE INDEX idx_users_active ON users(email) WHERE deleted_at IS NULL;// Con Prisma: soft delete via middleware
prisma.$use(async (params, next) => {
if (params.model === 'User') {
if (params.action === 'delete') {
params.action = 'update';
params.args.data = { deletedAt: new Date() };
}
if (params.action === 'findUnique' || params.action === 'findMany') {
params.args.where = { ...params.args.where, deletedAt: null };
}
}
return next(params);
});Arquitectura y sistema {#arquitectura}
31. ¿Cuál es la diferencia entre monolito y microservicios?
MONOLITO
+ Más simple de desarrollar al inicio
+ Un solo deploy
+ No hay latencia de red entre módulos
+ Transacciones simples (todo en la misma DB)
- Escala todo junto aunque solo un módulo tenga carga
- Un bug puede tirar todo el sistema
- Equipos grandes chocan en el mismo repo
MICROSERVICIOS
+ Cada servicio escala independientemente
+ Deploy independiente
+ Diferentes tecnologías por servicio
+ Fallos aislados
- Complejidad operacional (orchestration, service discovery, tracing)
- Consistencia eventual en lugar de ACID cross-servicio
- Latencia de red adicional
REGLA: empezá con monolito modular. Extraé microservicios cuando tenés
un bottleneck real medido, no por arquitectura aspiracional.32. ¿Cómo implementás un sistema de colas para jobs en background?
// Con BullMQ (Redis-based)
import { Queue, Worker, QueueEvents } from 'bullmq';
const connection = { host: 'localhost', port: 6379 };
// Producer: agregar jobs a la cola
const emailQueue = new Queue('emails', { connection });
async function sendWelcomeEmail(userId: string) {
await emailQueue.add(
'welcome',
{ userId },
{
delay: 1000, // esperar 1s antes de procesar
attempts: 3, // reintentar 3 veces si falla
backoff: {
type: 'exponential',
delay: 2000, // 2s, 4s, 8s entre reintentos
},
removeOnComplete: { count: 100 }, // limpiar jobs completados
}
);
}
// Consumer: procesar jobs
const emailWorker = new Worker(
'emails',
async (job) => {
const { userId } = job.data;
const user = await db.users.findById(userId);
await mailer.send({
to: user.email,
template: 'welcome',
data: { name: user.name },
});
console.log(`Email enviado a ${user.email}`);
},
{ connection, concurrency: 5 } // 5 jobs en paralelo
);
emailWorker.on('failed', (job, err) => {
console.error(`Job ${job.id} falló:`, err.message);
// Alertar en Sentry/Slack si agotó los reintentos
});33. ¿Qué es CORS y cómo lo configurás?
import cors from 'cors';
// Configuración estricta para producción
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://tuapp.com',
'https://www.tuapp.com',
// Solo en desarrollo:
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origen no permitido: ${origin}`));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // necesario si usás cookies httpOnly
maxAge: 86400, // caché del preflight por 24 horas
};
app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); // preflight para todas las rutas34. ¿Cómo implementás paginación eficientemente?
// OFFSET pagination — simple pero lenta para páginas altas
async function getUsersOffset(page: number, limit: number) {
const offset = (page - 1) * limit;
return db.query(
'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
[limit, offset]
// Problema: para page=1000, la DB lee y descarta 999*limit filas
);
}
// CURSOR pagination — O(log n) para cualquier página, ideal para feeds
async function getUsersCursor(cursor?: string, limit = 20) {
const query = cursor
? 'SELECT * FROM users WHERE created_at < $1 ORDER BY created_at DESC LIMIT $2'
: 'SELECT * FROM users ORDER BY created_at DESC LIMIT $1';
const params = cursor ? [new Date(cursor), limit + 1] : [limit + 1];
const rows = await db.query(query, params);
const hasMore = rows.length > limit;
const data = rows.slice(0, limit);
const nextCursor = hasMore ? data[data.length - 1].created_at.toISOString() : null;
return { data, nextCursor, hasMore };
}
// Uso
const page1 = await getUsersCursor();
const page2 = await getUsersCursor(page1.nextCursor);35. ¿Cómo implementás búsqueda full-text?
-- PostgreSQL tiene full-text search nativo
ALTER TABLE posts ADD COLUMN search_vector tsvector;
-- Actualizar el vector con un trigger
CREATE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector = to_tsvector('spanish',
COALESCE(NEW.title, '') || ' ' || COALESCE(NEW.content, '')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_posts_search
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
-- Índice GIN para búsquedas eficientes
CREATE INDEX idx_posts_search ON posts USING GIN(search_vector);
-- Búsqueda con ranking
SELECT
id, title,
ts_rank(search_vector, query) AS rank,
ts_headline('spanish', content, query) AS excerpt
FROM posts, to_tsquery('spanish', 'entrevista & programación') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;36. ¿Qué es el patrón BFF (Backend for Frontend)?
El BFF es una capa de API dedicada para cada tipo de cliente (web, mobile, etc.)
en lugar de un API genérico que sirve a todos.
Sin BFF:
Mobile App → API genérico (devuelve 50 campos, mobile usa 10)
Web App → API genérico (devuelve 50 campos, web usa 30)
Con BFF:
Mobile App → BFF Mobile → APIs internas
Web App → BFF Web → APIs internas
Ventajas:
+ Cada cliente recibe exactamente lo que necesita
+ El BFF puede agregar/transformar datos de múltiples APIs
+ Equipos de frontend controlan su propio BFF
+ Optimizaciones específicas por plataforma (compresión, caching)
Cuándo usarlo:
- Clientes con necesidades muy diferentes
- Microservicios donde el frontend hace demasiados requests
- Equipos de frontend y backend separados37. ¿Cómo manejás logs en producción?
// Estructura de logs para que sean consultables (no printf debugging)
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }), // string en lugar de número
},
base: {
env: process.env.NODE_ENV,
service: 'api',
version: process.env.APP_VERSION,
},
});
// Estructurado: cada campo es queryable en el log aggregator
logger.info({ userId: '123', action: 'login', ip: req.ip }, 'User logged in');
logger.error({ err, orderId: '456', userId: '123' }, 'Payment failed');
// Middleware de request logging
app.use((req, res, next) => {
const start = Date.now();
const reqLogger = logger.child({ requestId: req.id });
req.log = reqLogger;
res.on('finish', () => {
reqLogger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
}, 'Request completed');
});
next();
});38. ¿Cómo desplegás una aplicación Node.js a producción?
# Multi-stage build — imagen final más pequeña y segura
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
# Usuario no-root por seguridad
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER appuser
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]# docker-compose.yml para el stack completo
services:
api:
build: .
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
redis:
image: redis:7-alpine
volumes:
pgdata:TypeScript y herramientas {#typescript}
39. ¿Qué es TypeScript y cuáles son sus features más útiles?
// Generics — código reutilizable con tipos seguros
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number
const str = first(['a', 'b']); // type: string
// Utility types
type User = { id: string; name: string; email: string; password: string };
type PublicUser = Omit<User, 'password'>; // sin password
type PartialUser = Partial<User>; // todo opcional
type RequiredUser = Required<Partial<User>>; // todo requerido
type UserPreview = Pick<User, 'id' | 'name'>; // solo esos campos
// Discriminated unions — type narrowing seguro
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
function processResult<T>(result: Result<T>) {
if (result.ok) {
// TypeScript sabe que result.data existe aquí
console.log(result.data);
} else {
// Y que result.error existe aquí
console.error(result.error.message);
}
}
// Template literal types
type EventName = `on${Capitalize<string>}`;
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type Endpoint = `/${string}`;
type Route = `${HttpMethod} ${Endpoint}`;40. ¿Qué es Zod y para qué sirve?
import { z } from 'zod';
// Definir schema una vez → validación + tipos de TypeScript
const CreateUserSchema = z.object({
name: z.string().min(2, 'Nombre muy corto').max(100),
email: z.string().email('Email inválido').toLowerCase(),
age: z.number().int().min(18, 'Debe ser mayor de edad').max(120).optional(),
role: z.enum(['admin', 'user', 'moderator']).default('user'),
tags: z.array(z.string()).max(10).optional().default([]),
});
// Tipo inferido automáticamente — no hay duplicación
type CreateUserDto = z.infer<typeof CreateUserSchema>;
// Validación en runtime
function validateBody(schema: z.ZodSchema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
fields: result.error.flatten().fieldErrors,
},
});
}
req.body = result.data; // datos ya transformados/sanitizados
next();
};
}
app.post('/users', validateBody(CreateUserSchema), createUser);41. ¿Cómo escribís tests con Vitest?
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../services/UserService';
// Mock del repositorio
const mockUserRepo = {
findById: vi.fn(),
findByEmail: vi.fn(),
create: vi.fn(),
};
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
vi.clearAllMocks();
service = new UserService(mockUserRepo);
});
describe('getProfile', () => {
it('devuelve el usuario cuando existe', async () => {
const mockUser = { id: '1', name: 'Test User', email: 'test@example.com' };
mockUserRepo.findById.mockResolvedValue(mockUser);
const result = await service.getProfile('1');
expect(result).toEqual(mockUser);
expect(mockUserRepo.findById).toHaveBeenCalledWith('1');
expect(mockUserRepo.findById).toHaveBeenCalledTimes(1);
});
it('lanza NotFoundError cuando el usuario no existe', async () => {
mockUserRepo.findById.mockResolvedValue(null);
await expect(service.getProfile('999')).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
it('hashea la contraseña antes de guardar', async () => {
mockUserRepo.findByEmail.mockResolvedValue(null);
mockUserRepo.create.mockImplementation(async (dto) => ({ ...dto, id: '1' }));
await service.createUser({ email: 'new@example.com', password: 'raw123' });
const createCall = mockUserRepo.create.mock.calls[0][0];
expect(createCall.password).not.toBe('raw123'); // no guardó la password en claro
expect(createCall.password).toMatch(/^\$2b\$/); // es un hash bcrypt
});
});
});42. ¿Qué es el patrón Dependency Injection y por qué importa?
// SIN DI: acoplamiento duro, imposible de testear
class UserService {
async getUser(id: string) {
// Imposible mockear esto en tests
const db = new PostgresDatabase(process.env.DATABASE_URL);
return db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
// CON DI: las dependencias se inyectan desde afuera
class UserService {
constructor(
private readonly db: DatabasePort, // interfaz, no implementación concreta
private readonly cache: CachePort,
private readonly logger: LoggerPort,
) {}
async getUser(id: string) {
const cached = await this.cache.get(`user:${id}`);
if (cached) return cached;
const user = await this.db.findOne('users', { id });
await this.cache.set(`user:${id}`, user, 300);
return user;
}
}
// En producción: inyectás la implementación real
const service = new UserService(postgresDb, redisCache, pinoLogger);
// En tests: inyectás mocks
const service = new UserService(mockDb, mockCache, mockLogger);Comportamiento y diseño de sistemas {#comportamiento}
43. Diseñá un sistema de short URL (tipo bit.ly)
Preguntas que hacés antes de responder:
- ¿Cuántos redirects por segundo esperamos? (10k QPS, 100k QPS?)
- ¿Cuántos URLs nuevos por día?
- ¿Necesitamos analytics (clicks por URL)?
- ¿Los URLs expiran?
Diseño básico:
1. Generar el short code
- Tomar los primeros 7 chars del base62(hash(longUrl + timestamp))
- base62 = a-z, A-Z, 0-9 → 62^7 = 3.5 billones de combinaciones
- Verificar colisión en DB; si hay, regenerar
2. Schema
urls: id, short_code (unique), long_url, user_id, created_at, expires_at
clicks: url_id, timestamp, ip, user_agent, referrer, country
3. API
POST /shorten { url, customCode?, expiresIn? } → { shortUrl }
GET /:code → 301 redirect + log click async
4. Escalado
- Caché agresivo de shortCode → longUrl (Redis, TTL = 24h)
- El 99% del tráfico es lectura → múltiples réplicas
- Analytics en cola asíncrona (no bloquear el redirect)
- CDN para los redirects (edge functions)// Implementación del core
async function shorten(longUrl: string, userId?: string): Promise<string> {
// Reusar si ya existe
const existing = await db.urls.findByLongUrl(longUrl);
if (existing) return existing.shortCode;
let shortCode: string;
let attempts = 0;
do {
shortCode = generateCode(longUrl + Date.now() + attempts++);
if (attempts > 10) throw new Error('No se pudo generar código único');
} while (await db.urls.existsByCode(shortCode));
await db.urls.create({ shortCode, longUrl, userId });
return shortCode;
}
async function redirect(shortCode: string, meta: ClickMeta): Promise<string> {
const cacheKey = `url:${shortCode}`;
let longUrl = await redis.get(cacheKey);
if (!longUrl) {
const url = await db.urls.findByCode(shortCode);
if (!url) throw new NotFoundError('URL');
longUrl = url.longUrl;
await redis.setEx(cacheKey, 86400, longUrl);
}
// Log click en background — no bloquear el redirect
clickQueue.add('log', { shortCode, ...meta });
return longUrl;
}44. ¿Cómo diseñás un sistema de autenticación con OAuth 2.0?
OAuth 2.0 Authorization Code Flow (el más seguro para web apps):
1. Usuario hace click en "Login con Google"
2. Tu app redirige a Google con:
- client_id, redirect_uri, scope, state (CSRF token), code_challenge (PKCE)
3. Google muestra su login
4. Usuario autoriza → Google redirige a tu app con un `code`
5. Tu app intercambia code + code_verifier por access_token + id_token
6. Validás el id_token (JWT firmado por Google)
7. Creás/actualizás el usuario en tu DB con el sub (Google user ID)
8. Generás tu propio session token / JWT para la sesión
PKCE (Proof Key for Code Exchange) — obligatorio en apps públicas:
code_verifier = random string de 43-128 chars
code_challenge = BASE64URL(SHA256(code_verifier))
Enviás code_challenge en el paso 2
Enviás code_verifier en el paso 5
→ Previene que un atacante intercepte el code y lo use45. ¿Cuál fue tu error técnico más grande y qué aprendiste?
Esta pregunta evalúa madurez y autocrítica, no perfección.
Estructura de respuesta (STAR):
- Situación: qué estabas construyendo
- Tarea: tu responsabilidad específica
- Acción: qué hiciste (incluyendo el error)
- Resultado: impacto + corrección + lo que cambiaste después
Ejemplo honesto:
"Deployé un cambio de schema en producción sin migration correcta.
Dropped una columna que creía redundante. Resultó en que el job de
facturación fallaba silenciosamente hace 2 horas antes de que alguien
lo notara. Restauré desde backup en 45 minutos.
Después de eso: implementamos review obligatorio para toda migration
de DB, tests de integración que corren contra una DB real con datos de
prueba similares a producción, y alertas en Datadog para jobs que no
emiten su heartbeat cada 10 minutos."46. ¿Cómo enfocás la revisión de código de otro?
Lo que el entrevistador quiere escuchar:
1. Primero entender antes de criticar
- Preguntar "¿qué problema resuelve esto?" antes de sugerir reescrituras
- Leer el contexto del ticket/PR description
2. Distinguir entre obligatorio y sugerido
- MUST: bug real, security issue, viola convención del equipo
- SHOULD: mejora de legibilidad, mejor abstracción
- NIT: preferencia personal (a veces no vale el ciclo de review)
3. Comentarios accionables y específicos
- No: "esto es complicado"
- Sí: "este loop hace O(n²) — podemos usar un Map para hacerlo O(n):
const lookup = new Map(items.map(i => [i.id, i]));"
4. Aprobar con confianza cuando está bien
- No bloquear por nits
- Aprobar después de que el autor atiende los MUST47. ¿Cómo diseñás un sistema de notificaciones?
// Requisitos típicos: email, push, in-app, SMS
// El patrón: evento → cola → handlers por canal
// 1. Emitir evento desde el dominio (sin saber cómo se va a notificar)
await eventBus.emit('order.completed', {
orderId: order.id,
userId: order.userId,
total: order.total,
});
// 2. Handlers independientes por canal
eventBus.on('order.completed', async (event) => {
const user = await db.users.findById(event.userId);
const prefs = await db.notifPrefs.findByUserId(event.userId);
const notifications = [];
if (prefs.email) {
notifications.push(emailQueue.add('order-completed', {
to: user.email,
orderId: event.orderId,
total: event.total,
}));
}
if (prefs.push && user.pushToken) {
notifications.push(pushQueue.add('order-completed', {
token: user.pushToken,
title: 'Pedido confirmado',
body: `Tu pedido por $${event.total} está en camino`,
}));
}
// In-app siempre
notifications.push(db.notifications.create({
userId: event.userId,
type: 'ORDER_COMPLETED',
data: { orderId: event.orderId },
read: false,
}));
await Promise.allSettled(notifications); // fallo en uno no cancela los otros
});
// 3. API para marcar como leída
// PATCH /notifications/:id { read: true }
// GET /notifications?unread=true&page=1Recursos para seguir preparándote
Preparar una entrevista Full-Stack requiere práctica repetida, no solo leer respuestas. Las preguntas de esta guía cubren los temas más frecuentes, pero cada empresa tiene su propio estilo de evaluación.
Lo más efectivo para el día de la entrevista: practicar en voz alta hasta que las respuestas te salgan sin buscar las palabras. La diferencia entre "saber" algo y poder explicarlo bajo presión es más grande de lo que parece.
Temas para profundizar después de esta guía:
- Algoritmos y estructuras de datos (LeetCode Medium)
- System Design de escala (Designing Data-Intensive Applications — Kleppmann)
- Seguridad web: OWASP Top 10
- Testing avanzado: mocks vs stubs vs spies, contract testing
- CI/CD: GitHub Actions, deployment pipelines