Skip to content
← Back to Blog
Backend

From Frontend to Full-Stack: What Actually Changes When You Own the Whole System De Frontend a Full-Stack: Lo Que Cambia de Verdad Cuando Eres Dueño de Todo el Sistema

April 5, 2026 5 de abril de 2026

From Frontend to Full-Stack: What Actually Changes When You Own the Whole System

TL;DR: Owning a full system is not “frontend plus a few API routes.” It’s a different discipline with different failure modes, different mental models, and different stakes. This post is about what actually shifts when you cross that line — drawn from shipping complete systems with Node.js, Docker, Traefik, Supabase, and production observability.


I started as an Angular specialist. I knew change detection cold. I could tell you why OnPush wasn’t re-rendering before you finished the sentence. I built interfaces for Disney, Royal Caribbean, and United Airlines at Globant — complex, high-traffic SPAs where the frontend was the product.

Then I started owning complete systems. And I discovered that everything I knew was still useful, but none of it was sufficient.

This is not a “backend is harder than frontend” take. It’s more specific than that.

The Mindset Shift Nobody Names

On the frontend, your worst case is a broken render. The user sees something wrong, you fix it, you deploy. The feedback loop is visual and immediate.

On the backend — and especially once you include infrastructure — your worst case is silent data corruption, a cascade failure at 3am, or a security breach that happened six hours ago and you’re only finding out now.

The shift is from “does it look right” to “is it actually correct, right now, even when no one is watching.”

This sounds obvious. It isn’t. The instinct to reach for the browser and verify visually is so ingrained in frontend developers that it takes months to replace it with the instinct to reach for logs, metrics, and traces.

The Stack I Ended Up With

Before going into lessons, here is the actual stack I built around. This matters because “full-stack” can mean a hundred different things:

  • API layer: Node.js + Express (TypeScript)
  • Database: Supabase (PostgreSQL) and SQL Server / MySQL for legacy systems
  • Infrastructure: VPS with Docker Compose, Traefik as reverse proxy, Watchtower for automated container updates
  • Observability: Datadog (metrics + APM), Sentry (errors + traces), Microsoft Clarity (session replay)
  • Analytics: Umami (self-hosted, privacy-first), Google Analytics

Not Kubernetes. Not microservices. A single VPS running Docker Compose, properly configured, handles significant traffic and costs a fraction of what cloud-managed services would. That is a deliberate choice with real tradeoffs — more on that later.

What Traefik Changed About How I Think About Routing

Before running my own reverse proxy, I thought of routing as an application-level concern. You define routes in Express, they do what you say, done.

Traefik changed this completely. When Traefik is in front of your services, routing becomes infrastructure-level. TLS termination, automatic Let’s Encrypt certificate renewal, service discovery via Docker labels, load balancing, middleware for rate limiting and authentication — none of this lives in your application code.

# docker-compose.yml — a realistic Traefik + Express setup
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt

  api:
    build: ./api
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.yourdomain.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls.certresolver=letsencrypt"
      # Redirect HTTP → HTTPS at the infrastructure level
      - "traefik.http.routers.api-http.rule=Host(`api.yourdomain.com`)"
      - "traefik.http.routers.api-http.entrypoints=web"
      - "traefik.http.routers.api-http.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

volumes:
  letsencrypt:

The key insight: your Express app does not know it is behind Traefik. It listens on its container port and does its job. Infrastructure concerns are separated at the infrastructure layer. This is separation of concerns applied at the system level, not the code level.

The gotcha that bites everyone: when Traefik terminates TLS, req.secure in Express will be false even for HTTPS requests, because Express is receiving HTTP from the proxy. You need to trust the X-Forwarded-Proto header:

// In your Express app setup — required when behind a reverse proxy
app.set('trust proxy', 1)

// Now req.secure and req.ip work correctly
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.headers.host}${req.url}`)
  }
  next()
})

Error Handling Is Not Optional

On the frontend, an unhandled error crashes a component. On the backend, an unhandled promise rejection in Express (before Node.js 15) would silently swallow the error and leave the request hanging.

// This is NOT enough — async errors are not caught by Express by default
app.get('/users/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
  res.json(user)
  // If db.query throws, Express never sees it — the request hangs in older versions
})

The correct pattern is to wrap every async route handler, or use a utility that does it for you:

// Option A: explicit try/catch — verbose but explicit
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
    res.json(user)
  } catch (err) {
    next(err) // passes to Express error middleware
  }
})

// Option B: wrapper utility — less noise
const asyncHandler = (fn: RequestHandler): RequestHandler =>
  (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
  res.json(user)
}))

// Centralized error handler — always define this
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
  console.error(err.stack)
  res.status(500).json({
    error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message
  })
})

Note: Express 5 (stable as of 2024) handles async errors natively — next(err) is called automatically for rejected promises. If you are on Express 5, the wrapper is unnecessary. If you are on Express 4, it is not optional.

Observability: The Thing Frontend Developers Underestimate Most

On the frontend, the browser DevTools are your observability layer. They are always there, always free, always showing you everything.

On the backend, there is no DevTools. There is only what you instrument.

I use a three-layer observability setup:

  1. Sentry — captures exceptions with full stack traces, request context, and breadcrumbs. Fires an alert the moment something breaks in production.
  2. Datadog — metrics, APM traces, infrastructure monitoring. Answers “is the API slow right now?” and “which endpoint is consuming 80% of CPU?”
  3. Microsoft Clarity — session replay for the frontend. When a user reports a bug, you watch exactly what they did.

Setting up Sentry in a Node.js/Express API is straightforward, but the initialization order matters:

import * as Sentry from '@sentry/node'
import express from 'express'

// Sentry MUST be initialized before importing anything else
// that might throw — especially database clients
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
  // Capture 10% of traces in production — 100% in dev/staging
})

const app = express()

// Sentry request handler — must be first middleware
app.use(Sentry.Handlers.requestHandler())
// Sentry tracing handler
app.use(Sentry.Handlers.tracingHandler())

// ... your routes ...

// Sentry error handler — must be before your own error handler
app.use(Sentry.Handlers.errorHandler())

// Your error handler comes after Sentry's
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
  res.status(500).json({ error: 'Internal server error' })
})

The tracesSampleRate is important. In production, capturing 100% of traces generates significant volume and cost. 10% gives you a statistically meaningful sample without bankrupting the observability budget. Adjust based on your traffic.

Watchtower and the Automated Deployment Problem

One pattern I use that surprises frontend developers: automated container updates via Watchtower.

# docker-compose.yml addition
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 30 --cleanup
    # Checks for new images every 30 seconds, removes old ones

Watchtower watches your running containers. When you push a new image to your registry, Watchtower detects it, pulls the new image, and restarts the container with zero downtime (with the --rolling-restart flag).

Your CI/CD pipeline becomes: build → push image → Watchtower deploys automatically.

This is appropriate for personal projects and small teams where the deployment risk is low and the convenience is high. For production systems with strict uptime requirements, you want more control over deployment timing — blue/green deployments, health checks before traffic cutover, and rollback automation. Watchtower trades control for simplicity.

Know what you’re trading. That’s the full-stack mindset.

The Database Is Not Your Frontend State Manager

This one tripped me up early. On the frontend, you are used to optimistic updates — update the local state immediately, sync with the server later. The UI feels fast. If the sync fails, you roll back.

On the backend, you are the server. There is no “later.” Either the data is consistent right now, or it is not.

// This is wrong — two operations with no atomicity guarantee
async function transferBalance(fromId: string, toId: string, amount: number) {
  await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId])
  // If the process crashes here, money is gone from one account but not added to the other
  await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId])
}

// This is correct — a transaction guarantees atomicity
async function transferBalance(fromId: string, toId: string, amount: number) {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    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')
  } catch (err) {
    await client.query('ROLLBACK')
    throw err
  } finally {
    client.release()
  }
}

The financial example is dramatic but the principle applies anywhere: whenever two or more database operations must succeed or fail together, use a transaction. This is not a performance optimization — it is a correctness requirement.

The Tradeoffs I Accepted

I promised to be honest about the tradeoffs of this stack.

VPS + Docker Compose vs. managed cloud:

VPS + Docker ComposeManaged Cloud (Render, Railway, ECS)
CostLow (fixed monthly)Higher (scales with usage)
ControlFull — you configure everythingLimited to what the platform exposes
MaintenanceYou own server updates, security patchesPlatform handles it
ScalingManual — add a server, update ComposeAutomatic
Failure recoveryYou fix itPlatform handles container restarts

For projects with moderate traffic and a solo developer or small team, VPS + Docker Compose is a rational choice. For a startup that needs to scale unpredictably, or a team without infrastructure experience, managed platforms reduce operational risk.

Neither is universally correct. The mistake is not making a deliberate choice.

What the Frontend Background Actually Gave Me

After all the “it’s different and harder” — what carried over?

Component thinking. The habit of decomposing a problem into small, independent units maps perfectly to service design and middleware composition. An Express middleware chain is architecturally identical to Angular’s dependency injection pipeline: small functions with single responsibilities, composed in order.

State management discipline. Years of thinking carefully about where state lives, who owns it, and how mutations propagate made me much better at database schema design. The questions are the same — just at a different layer.

User empathy. Backend developers who have never built frontends often design APIs that are technically correct but miserable to consume. Having lived on both sides, I know what response shapes are actually useful, what error messages actually help, and why latency matters more than the server-side code suggests.

The jump is real. But the foundation is not wasted.

Key Takeaways

  • The mental model shift is the hardest part: from “does it look right” to “is it actually correct when no one is watching.”
  • Traefik belongs at the infrastructure layer, not the application layer: TLS, HTTPS redirects, and routing are not your Express app’s job.
  • Async error handling in Express requires explicit work — either try/catch + next(err), or Express 5.
  • Instrument before you need it: Sentry + Datadog on day one, not after the first production incident.
  • Transactions are a correctness requirement, not an optimization: if two DB operations must succeed together, wrap them in a transaction.
  • Know your tradeoffs: VPS + Docker Compose, managed platform, and Kubernetes each make sense in different contexts. The mistake is not choosing deliberately.

Further Reading

De Frontend a Full-Stack: Lo Que Cambia de Verdad Cuando Eres Dueño de Todo el Sistema

TL;DR: Ser dueño de un sistema completo no es “frontend más unos endpoints.” Es una disciplina diferente, con modos de falla distintos, modelos mentales distintos, y consecuencias distintas. Este post es sobre qué cambia de verdad cuando cruzas esa línea — basado en haber shippeado sistemas completos con Node.js, Docker, Traefik, Supabase, y observabilidad en producción.


Empecé como especialista en Angular. Conocía el change detection de memoria. Podía decirte por qué OnPush no estaba re-renderizando antes de que terminaras la pregunta. Construí interfaces para Disney, Royal Caribbean y United Airlines en Globant — SPAs complejas y de alto tráfico donde el frontend era el producto.

Después empecé a ser dueño de sistemas completos. Y descubrí que todo lo que sabía seguía siendo útil, pero nada de eso era suficiente.

Este no es un “el backend es más difícil que el frontend.” Es algo más específico que eso.

El Cambio de Mindset Que Nadie Nombra

En el frontend, tu peor caso es un render roto. El usuario ve algo mal, lo corriges, despliegas. El feedback loop es visual e inmediato.

En el backend — y especialmente una vez que incluyes infraestructura — tu peor caso es corrupción silenciosa de datos, un cascade failure a las 3am, o una brecha de seguridad que ocurrió hace seis horas y te estás enterando recién ahora.

El cambio es de “¿se ve bien?” a “¿es realmente correcto, ahora mismo, incluso cuando nadie lo está mirando?”

Suena obvio. No lo es. El instinto de abrir el navegador y verificar visualmente está tan arraigado en los desarrolladores de frontend que lleva meses reemplazarlo con el instinto de buscar logs, métricas y trazas.

El Stack Con el Que Terminé

Antes de entrar en las lecciones, el stack real que construí. Esto importa porque “full-stack” puede significar cien cosas distintas:

  • Capa API: Node.js + Express (TypeScript)
  • Base de datos: Supabase (PostgreSQL) y SQL Server / MySQL para sistemas legacy
  • Infraestructura: VPS con Docker Compose, Traefik como reverse proxy, Watchtower para actualizaciones automáticas de contenedores
  • Observabilidad: Datadog (métricas + APM), Sentry (errores + trazas), Microsoft Clarity (session replay)
  • Analytics: Umami (self-hosted, privacy-first), Google Analytics

Sin Kubernetes. Sin microservicios. Un VPS corriendo Docker Compose, bien configurado, maneja tráfico significativo y cuesta una fracción de lo que costarían servicios manejados en la nube. Es una decisión deliberada con tradeoffs reales — más sobre eso después.

Lo Que Traefik Cambió en Cómo Pienso el Ruteo

Antes de correr mi propio reverse proxy, pensaba en el ruteo como una preocupación de la capa de aplicación. Defines las rutas en Express, hacen lo que dices, listo.

Traefik cambió esto completamente. Cuando Traefik está delante de tus servicios, el ruteo se convierte en infraestructura. La terminación de TLS, la renovación automática de certificados Let’s Encrypt, el service discovery via Docker labels, el balanceo de carga, el middleware para rate limiting y autenticación — nada de esto vive en tu código de aplicación.

# docker-compose.yml — un setup realista de Traefik + Express
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.email=tu@email.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt

  api:
    build: ./api
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.tudominio.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls.certresolver=letsencrypt"
      # Redirigir HTTP → HTTPS a nivel de infraestructura
      - "traefik.http.routers.api-http.rule=Host(`api.tudominio.com`)"
      - "traefik.http.routers.api-http.entrypoints=web"
      - "traefik.http.routers.api-http.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

volumes:
  letsencrypt:

La insight clave: tu app Express no sabe que está detrás de Traefik. Escucha en su puerto de contenedor y hace su trabajo. Las preocupaciones de infraestructura están separadas en la capa de infraestructura. Es separación de responsabilidades aplicada a nivel de sistema, no de código.

El gotcha que le pica a todos: cuando Traefik termina TLS, req.secure en Express va a ser false incluso para requests HTTPS, porque Express está recibiendo HTTP del proxy. Necesitas confiar en el header X-Forwarded-Proto:

// En el setup de tu app Express — requerido cuando estás detrás de un reverse proxy
app.set('trust proxy', 1)

// Ahora req.secure y req.ip funcionan correctamente
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.headers.host}${req.url}`)
  }
  next()
})

El Manejo de Errores No Es Opcional

En el frontend, un error no manejado rompe un componente. En el backend, un promise rejection no manejado en Express (antes de Node.js 15) podía tragarse silenciosamente el error y dejar el request colgado.

// Esto NO es suficiente — los errores async no son capturados por Express por defecto
app.get('/users/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
  res.json(user)
  // Si db.query lanza, Express nunca lo ve — el request queda colgado en versiones antiguas
})

El patrón correcto es envolver cada route handler async, o usar una utilidad que lo haga:

// Opción A: try/catch explícito — verboso pero claro
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
    res.json(user)
  } catch (err) {
    next(err) // pasa al middleware de error de Express
  }
})

// Opción B: utilidad wrapper — menos ruido
const asyncHandler = (fn: RequestHandler): RequestHandler =>
  (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id])
  res.json(user)
}))

// Manejador centralizado de errores — siempre define esto
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
  console.error(err.stack)
  res.status(500).json({
    error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message
  })
})

Nota: Express 5 (estable desde 2024) maneja errores async de forma nativa — next(err) se llama automáticamente para promises rechazados. Si estás en Express 5, el wrapper es innecesario. Si estás en Express 4, no es opcional.

Observabilidad: Lo Que los Desarrolladores Frontend Más Subestiman

En el frontend, el DevTools del navegador es tu capa de observabilidad. Siempre está ahí, siempre gratis, siempre mostrándote todo.

En el backend, no hay DevTools. Solo hay lo que tú instrumentas.

Uso una configuración de observabilidad en tres capas:

  1. Sentry — captura excepciones con stack traces completos, contexto del request y breadcrumbs. Dispara una alerta en el momento en que algo se rompe en producción.
  2. Datadog — métricas, trazas APM, monitoreo de infraestructura. Responde “¿la API está lenta ahora?” y “¿qué endpoint está consumiendo el 80% de CPU?”
  3. Microsoft Clarity — session replay para el frontend. Cuando un usuario reporta un bug, ves exactamente qué hizo.

Configurar Sentry en una API Node.js/Express es directo, pero el orden de inicialización importa:

import * as Sentry from '@sentry/node'
import express from 'express'

// Sentry DEBE inicializarse antes de importar cualquier otra cosa
// que pueda lanzar — especialmente clientes de base de datos
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
  // Captura el 10% de trazas en producción — 100% en dev/staging
})

const app = express()

// Request handler de Sentry — debe ser el primer middleware
app.use(Sentry.Handlers.requestHandler())
// Tracing handler de Sentry
app.use(Sentry.Handlers.tracingHandler())

// ... tus rutas ...

// Error handler de Sentry — debe ir antes de tu propio manejador de errores
app.use(Sentry.Handlers.errorHandler())

// Tu manejador de errores va después del de Sentry
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
  res.status(500).json({ error: 'Internal server error' })
})

El tracesSampleRate importa. En producción, capturar el 100% de trazas genera un volumen significativo y costo. El 10% te da una muestra estadísticamente significativa sin fundir el presupuesto de observabilidad. Ajústalo según tu tráfico.

Watchtower y el Problema del Despliegue Automatizado

Un patrón que uso y que sorprende a los desarrolladores frontend: actualizaciones automáticas de contenedores via Watchtower.

# Adición al docker-compose.yml
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 30 --cleanup
    # Verifica nuevas imágenes cada 30 segundos, elimina las viejas

Watchtower observa tus contenedores en ejecución. Cuando haces push de una imagen nueva a tu registry, Watchtower la detecta, la descarga, y reinicia el contenedor sin tiempo de inactividad (con el flag --rolling-restart).

Tu pipeline de CI/CD se convierte en: build → push de imagen → Watchtower despliega automáticamente.

Esto es apropiado para proyectos personales y equipos pequeños donde el riesgo de despliegue es bajo y la conveniencia es alta. Para sistemas de producción con requisitos estrictos de uptime, necesitas más control sobre el timing del despliegue — blue/green deployments, health checks antes del corte de tráfico, y automatización del rollback. Watchtower cambia control por simplicidad.

Ten claro lo que estás intercambiando. Ese es el mindset full-stack.

La Base de Datos No Es Tu State Manager del Frontend

Esta me golpeó temprano. En el frontend, estás acostumbrado a los optimistic updates — actualizas el estado local inmediatamente, sincronizas con el servidor después. La UI se siente rápida. Si la sincronización falla, reviertes.

En el backend, tú eres el servidor. No hay “después.” O los datos son consistentes ahora mismo, o no lo son.

// Esto está mal — dos operaciones sin garantía de atomicidad
async function transferBalance(fromId: string, toId: string, amount: number) {
  await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId])
  // Si el proceso se cae acá, el dinero salió de una cuenta pero no entró en la otra
  await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId])
}

// Esto está bien — una transacción garantiza la atomicidad
async function transferBalance(fromId: string, toId: string, amount: number) {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    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')
  } catch (err) {
    await client.query('ROLLBACK')
    throw err
  } finally {
    client.release()
  }
}

El ejemplo financiero es dramático pero el principio aplica en cualquier lado: cuando dos o más operaciones de base de datos deben tener éxito o fallar juntas, usa una transacción. Esto no es una optimización de rendimiento — es un requisito de correctitud.

Los Tradeoffs Que Acepté

Prometí ser honesto sobre los tradeoffs de este stack.

VPS + Docker Compose vs. cloud manejado:

VPS + Docker ComposeCloud Manejado (Render, Railway, ECS)
CostoBajo (mensual fijo)Más alto (escala con el uso)
ControlTotal — configuras todo túLimitado a lo que expone la plataforma
MantenimientoEres responsable de actualizaciones del servidor y parches de seguridadLa plataforma lo maneja
EscaladoManual — agregas un servidor, actualizas ComposeAutomático
Recuperación de fallasTú lo arreglasLa plataforma maneja reinicios de contenedores

Para proyectos con tráfico moderado y un solo desarrollador o equipo pequeño, VPS + Docker Compose es una elección racional. Para una startup que necesita escalar de forma impredecible, o un equipo sin experiencia en infraestructura, las plataformas manejadas reducen el riesgo operacional.

Ninguno es universalmente correcto. El error es no tomar una decisión deliberada.

Lo Que el Background de Frontend Realmente Me Dio

Después de todo el “es diferente y más difícil” — ¿qué se transfirió?

El pensamiento en componentes. El hábito de descomponer un problema en unidades pequeñas e independientes mapea perfectamente al diseño de servicios y la composición de middlewares. Una cadena de middlewares de Express es arquitectónicamente idéntica al pipeline de inyección de dependencias de Angular: funciones pequeñas con responsabilidades únicas, compuestas en orden.

La disciplina del manejo de estado. Años de pensar cuidadosamente sobre dónde vive el estado, quién es su dueño, y cómo se propagan las mutaciones, me hicieron mucho mejor en el diseño de schemas de base de datos. Las preguntas son las mismas — solo en una capa diferente.

La empatía con el usuario. Los desarrolladores backend que nunca construyeron frontends frecuentemente diseñan APIs que son técnicamente correctas pero miserables de consumir. Habiendo vivido en ambos lados, sé qué formas de respuesta son realmente útiles, qué mensajes de error realmente ayudan, y por qué la latencia importa más de lo que el código server-side sugiere.

El salto es real. Pero la base no es tiempo perdido.

Puntos Clave

  • El cambio de mindset es la parte más difícil: de “¿se ve bien?” a “¿es realmente correcto cuando nadie lo está mirando?”.
  • Traefik pertenece a la capa de infraestructura, no a la de aplicación: TLS, redirecciones HTTPS y ruteo no son el trabajo de tu app Express.
  • El manejo de errores async en Express requiere trabajo explícito — ya sea try/catch + next(err), o Express 5.
  • Instrumenta antes de necesitarlo: Sentry + Datadog desde el día uno, no después del primer incidente en producción.
  • Las transacciones son un requisito de correctitud, no una optimización: si dos operaciones de DB deben tener éxito juntas, envolverlas en una transacción no es negociable.
  • Conoce tus tradeoffs: VPS + Docker Compose, plataforma manejada, y Kubernetes tienen sentido en contextos distintos. El error es no elegir deliberadamente.

Para Leer Más

← Back to Blog