InterviewHack.ai
Start free
Blog/Preguntas de entrevista Full-Stack con código y respuestas (45+)

Preguntas de entrevista Full-Stack con código y respuestas (45+)

16 de septiembre de 2026

fullstackbackend

Artículo completo de 45+ preguntas de entrevista Full-Stack con código real y respuestas detalladas en español rioplatense

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.

js
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); // false

Regla 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.

js
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.

js
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); // undefined

Usos 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.

js
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 |

js
// 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 referencia

Regla: 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.

jsx
// 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> padre

Key prop: le dice a React cómo identificar elementos en listas para el diffing.

jsx
// 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.

jsx
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.

tsx
// 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`

js
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 / fallbacks

10. ¿Cómo optimizás el rendimiento de una aplicación React?

tsx
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?

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?

css
/* 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?

js
// 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?

js
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

ts
// 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?

js
// 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
js
// 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?

js
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?

js
// 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?

js
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?

js
// 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?

ts
// 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
-- 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;
js
// 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?

sql
-- 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?

js
// 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?

js
// 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?

js
// 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?

sql
-- 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 real

30. ¿Cómo implementás soft delete?

sql
-- 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;
ts
// 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?

ts
// 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?

js
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 rutas

34. ¿Cómo implementás paginación eficientemente?

ts
// 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?

sql
-- 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 separados

37. ¿Cómo manejás logs en producción?

ts
// 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?

dockerfile
# 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"]
yaml
# 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?

ts
// 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?

ts
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?

ts
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?

ts
// 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)
ts
// 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 use

45. ¿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 MUST

47. ¿Cómo diseñás un sistema de notificaciones?

ts
// 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=1

Recursos 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

FAQ

¿Cuántas preguntas cubre el artículo?+

El artículo cubre 47 preguntas numeradas organizadas en 6 secciones: Frontend (12), Backend Node.js (10), Bases de datos (8), Arquitectura (8), TypeScript (4) y Comportamiento/System Design (5).

¿Para qué nivel de experiencia es este artículo?+

Está orientado a desarrolladores Full-Stack con 1 a 5 años de experiencia que se preparan para entrevistas técnicas en empresas de tecnología. Cubre tanto conceptos fundamentales como patrones avanzados de arquitectura y diseño de sistemas.

¿Los ejemplos de código son reales o pseudocódigo?+

Todos los ejemplos son código real y ejecutable: JavaScript/TypeScript, SQL, CSS, y configuraciones de Docker/Docker Compose. Cada snippet incluye comentarios explicando el por qué, no solo el qué.

¿El artículo cubre preguntas de system design?+

Sí, las últimas preguntas (43-47) cubren diseño de sistemas: URL shortener, OAuth 2.0, sistema de notificaciones, y preguntas de comportamiento técnico. Incluyen el razonamiento antes del código, que es lo que el entrevistador realmente evalúa.

¿Qué stack tecnológico asume el artículo?+

El stack principal es React (frontend), Node.js/Express (backend), PostgreSQL (base de datos) y Redis (caché/colas). Las preguntas de conceptos (event loop, closures, closures, ACID, etc.) son agnósticas al stack.

¿Cómo usar este artículo para prepararse?+

Lo más efectivo es leer cada pregunta, intentar responderla en voz alta antes de ver el código, y luego comparar con la respuesta. Para las preguntas de system design, practicar diagramar en papel. La sección final del artículo tiene recursos para profundizar en cada área.

Related articles

Cómo usar el método STAR en entrevistas (con ejemplos reales)

Aprende a responder preguntas difíciles usando el método STAR en entrevistas. Consejos y ejemplos concretos para roles remotos tech de LATAM.

Cómo conseguir trabajo remoto en dólares desde LATAM: guía real

Descubre consejos concretos para conseguir trabajo remoto en dólares desde LATAM: estrategias de búsqueda, preparación y entrevista para roles tecnológicos.

Las mejores preguntas para hacerle al entrevistador al final

Descubre las mejores preguntas para hacerle al entrevistador al final, útiles para entrevistas tech remotas, diferenciándote y logrando roles en dólares.

Cómo preparar entrevistas de desarrollo sin experiencia previa

Consejos prácticos para enfrentar entrevistas de tu primer trabajo como desarrollador, incluso sin experiencia. Técnicas para destacar y convencer en cada etapa.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY