Cómo construimos Link: la arquitectura completa entra en una página
La mayoría de los acortadores "enterprise" describe su arquitectura con diagramas de quince cajas. La nuestra entra en este post. No por falta de ambición: porque un acortador de links no necesita más.
El redirect es un route handler, no una plataforma
Link es Next.js con App Router. Todo el producto pasa por un route handler que hace exactamente tres cosas: leer, registrar, redirigir.
// app/[slug]/route.ts
export function GET(req: NextRequest, { params }: { params: { slug: string } }) {
const link = getLink(params.slug); // SELECT por slug indexado
if (!link || expired(link) || maxedOut(link)) {
return new Response("Not found", { status: 404 });
}
if (link.password && !authorized(req, link)) {
return NextResponse.redirect(`/gate/${params.slug}`, 302);
}
recordClick(req, link.id); // INSERT de ~200 bytes
return NextResponse.redirect(link.url, 302);
}
Medido en el VPS más barato que encontramos: menos de 3 milisegundos de punta a punta, p99 incluido. La latencia que percibís es la de la red, no la nuestra.
SQLite, en serio
La base es SQLite con better-sqlite3, en modo WAL. Para esta carga es una decisión de ingeniería, no un atajo:
- Sincrónico y en proceso: sin red, sin pool de conexiones. Un
SELECTes un lookup de microsegundos. - Un archivo: backup es copiar el archivo. Restore es pegarlo. Migrar de servidor es un
scp. - Escala de sobra: millones de filas indexadas responden en milisegundos. Nuestro usuario más pesado está a décadas del límite.
Analytics sin cookies
No hay tracker, ni píxel, ni fingerprint. Todo sale de los headers que el browser ya manda en cada request:
function recordClick(req: NextRequest, linkId: string) {
const ua = parseUA(req.headers.get("user-agent")); // device, browser, os
db.prepare(`INSERT INTO clicks
(link_id, ts, referrer, country, device, browser, os)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(
linkId,
Date.now(),
domainOf(req.headers.get("referer")), // solo el dominio
req.headers.get("cf-ipcountry") ?? null, // país, sin tocar la IP
ua.device, ua.browser, ua.os
);
}
Guardamos el dominio del referrer, no la URL completa. El país viene del header del edge, no de geolocalizar la IP — que ni siquiera almacenamos.
QR on-demand
Los QR no se generan por adelantado ni se guardan: se calculan en cada request a /qr/[slug].svg. Es un algoritmo determinista sobre un string; almacenarlo sería pagar disco para guardar algo que se recomputa gratis en un milisegundo.
Lo que no usamos (y por qué)
- Redis: SQLite en WAL ya lee en microsegundos. Un cache sería otra pieza que mantener para ganar nada.
- Kafka / colas de eventos: el
INSERTde analytics es parte del request y cuesta menos de un milisegundo. Una cola es la solución a un problema que no tenemos. - Microservicios: un acortador es un proceso. Dividirlo en cinco convierte cada deploy en una coreografía y cada bug en una investigación distribuida.
La arquitectura aburrida es una feature: hay menos piezas que puedan fallar a las 3 de la mañana. Todo el código es abierto — auditá cada línea.