Un servicio fullstack para acortar URLs y generar códigos QR de forma instantánea. Frontend moderno con tema oscuro/claro e internacionalización, backend eficiente con Express.js y almacenamiento SQLite.
Status: ✅ Funcional | 🚀 Pronto en Vercel | 📝 Documentado
- 🔗 Acortador de URLs: Genera códigos cortos y únicos
- 🎨 Códigos QR: QR automático para cada URL acortada
- 🌍 Multi-idioma: Soporte para English y Español (i18n)
- 🌙 Tema oscuro/claro: Personalización por preferencia del usuario
- ⚡ Sin autenticación: Úsalo al instante, sin login
- 📊 Contador de hits: Registra cada redirección
- 🔄 Deduplicación: La misma URL original = mismo código corto
- 🚀 Pronto a producción: Deployment ready para Vercel
- Estructura del Proyecto
- Requisitos
- Inicio Rápido
- Desarrollo
- API REST
- Configuración
- Testing
- Deploy
- Troubleshooting
url-shortener-qr/
├── apps/
│ ├── backend/ # Express API (TypeScript)
│ │ ├── src/
│ │ │ ├── app.ts # Configuración Express
│ │ │ ├── server.ts # Punto de entrada
│ │ │ ├── config/ # Variables de entorno
│ │ │ ├── controllers/ # HTTP Handlers
│ │ │ ├── services/ # Lógica de negocio
│ │ │ ├── repositories/ # Acceso a datos
│ │ │ ├── routes/ # Definición de rutas
│ │ │ └── utils/ # Utilidades (QR, etc)
│ │ ├── tests/ # Tests unitarios
│ │ └── dist/ # Output compilado
│ │
│ ├── frontend/ # React + Vite
│ │ ├── src/
│ │ │ ├── components/ # Componentes React
│ │ │ ├── api/ # Cliente HTTP
│ │ │ ├── theme/ # Sistema de temas
│ │ │ ├── i18n/ # Traducciones
│ │ │ ├── App.tsx # Componente raíz
│ │ │ └── main.tsx # Entry point
│ │ ├── dist/ # Output build
│ │ └── vite.config.ts # Configuración Vite
│
├── packages/
│ └── database/ # Capa BD (sql.js)
│ ├── src/
│ │ ├── index.ts # API pública
│ │ ├── contracts.ts # Interfaces
│ │ └── sqlite-driver.ts # Driver SQLite
│ └── data/
│ └── dev.db # Base de datos
│
├── scripts/
│ └── copy-frontend-dist.mjs # Build script
├── package.json # Monorepo config
└── README.md # Este archivo
Workspaces:
apps/backend: API REST que acorta URLs, genera QR y redirige (GET /:code)apps/frontend: SPA que consume la API y muestra el QR generadopackages/database: Utilidades compartidas para SQLite con soporte para drivers modulares
- Node.js: v18+
- npm: v9+
- Git: Para clonar el repositorio
Verifica:
node --version # v18.0.0 o superior
npm --version # v9.0.0 o superiorgit clone https://github.com/xdextx/url-shortener-qr.git
cd url-shortener-qrnpm installEsto instala automáticamente todas las dependencias del monorepo.
# Backend
cp apps/backend/.env.example apps/backend/.env
# Frontend
cp apps/frontend/.env.example apps/frontend/.env
# Database
cp packages/database/.env.example packages/database/.envEdita los .env según tu entorno (ver sección Configuración).
npm run devEsto inicia:
- Backend: http://localhost:3000
- Frontend: http://localhost:5173
# Desarrollo simultáneo (backend + frontend)
npm run dev
# Servicios individuales
npm run dev:backend
npm run dev:frontend
# Build (todo)
npm run build
# Build individual
npm run build:frontend
npm run build:backend
npm run build:database
# Tests
npm run test
# Linting
npm run lint# 1. Inicia el entorno de desarrollo
npm run dev
# 2. Abre http://localhost:5173 en tu navegador
# 3. Haz cambios en el código
# Los cambios se actualizan automáticamente (hot reload)
# 4. Cuando termines, ejecuta tests
npm run test
# 5. Haz commit con mensaje descriptivo
git commit -m "feat: nueva funcionalidad"# Puerto del servidor
PORT=3000
# URL base para short URLs generados
BASE_URL=http://localhost:3000
# URL del frontend (para redirecciones)
FRONTEND_URL=http://localhost:5173
# Base de datos
DATABASE_URL=file:./data/dev.db
DATABASE_DRIVER=sqliteNotas:
BASE_URLse usa en la tabla de URLs (debe ser igual en producción)FRONTEND_URLes donde redirigeGET /si alguien visita la raíz- La raíz sirve el frontend compilado después del build
# URL de la API del backend
VITE_API_BASE_URL=http://localhost:3000
# Puerto de desarrollo
VITE_PORT=5173# Ruta de la base de datos
DATABASE_URL=file:./data/dev.db
# Driver a usar (sqlite es el default)
DATABASE_DRIVER=sqlitePOST /api/v1/urls
Crea un nuevo short URL. Si la URL original ya existe, retorna el código existente.
Request:
curl -X POST http://localhost:3000/api/v1/urls \
-H "Content-Type: application/json" \
-d '{"originalUrl":"https://www.example.com/very/long/path"}'Response: (201 Created)
{
"code": "abc123",
"shortUrl": "http://localhost:3000/abc123",
"originalUrl": "https://www.example.com/very/long/path",
"qrCodeDataUrl": "data:image/png;base64,iVBORw0KGgo..."
}GET /api/v1/urls/:code
Recupera detalles de un short URL.
curl http://localhost:3000/api/v1/urls/abc123Response: (200 OK)
{
"code": "abc123",
"shortUrl": "http://localhost:3000/abc123",
"originalUrl": "https://www.example.com/very/long/path",
"qrCodeDataUrl": "data:image/png;base64,..."
}GET /:code → Redirige al URL original e incrementa hits
curl -L http://localhost:3000/abc123
# Redirige a: https://www.example.com/very/long/pathGET /health
curl http://localhost:3000/health
# {"status":"ok"}# Todos los tests
npm run test
# Solo backend
npm --workspace @url-shortener/backend run test
# Watch mode
npm --workspace @url-shortener/backend run test:watch
# Con cobertura
npm run test -- --coverageLos tests van en apps/backend/tests/ y usan Vitest:
import { describe, it, expect, beforeEach } from "vitest";
import { createShortUrl } from "../src/services/url.service";
describe("createShortUrl", () => {
beforeEach(async () => {
await clearStore(); // Limpia BD antes de cada test
});
it("crea un short URL válido", async () => {
const result = await createShortUrl("https://example.com");
expect(result.originalUrl).toBe("https://example.com");
expect(result.code).toBeDefined();
expect(result.qrCodeDataUrl).toContain("data:image/png");
});
});npm run buildGenera:
- Backend compilado en
apps/backend/dist/ - Frontend compilado en
apps/frontend/dist/ - Frontend copiado a
dist/para servir desde backend
npm run build
npm run start # Usa dist/ del backendEl servidor sirve:
/api/*→ API del backend/*→ Frontend (SPA)
Crea una BD PostgreSQL gratuita en:
Copia la connection string (ej: postgresql://user:pass@host/dbname)
npm i -g vercel
vercel link # Conecta tu repo a VercelSettings → Environment Variables:
PORT=3000
BASE_URL=https://tu-app.vercel.app
FRONTEND_URL=https://tu-app.vercel.app
VITE_API_BASE_URL=https://tu-app.vercel.app
DATABASE_URL=postgresql://user:pass@host/dbname
DATABASE_DRIVER=postgres
Nota: Reemplaza tu-app.vercel.app con tu dominio real en Vercel
Cada push a main se deploya automáticamente:
git push origin main
# Vercel automáticamente:
# - Instala deps
# - Corre npm run build
# - Compila vercel.ts
# - Deploya# Health check
curl https://tu-app.vercel.app/health
# {"status":"ok"}
# Test crear short URL
curl -X POST https://tu-app.vercel.app/api/v1/urls \
-H "Content-Type: application/json" \
-d '{"originalUrl":"https://example.com"}'Estructura en Vercel:
/→ Frontend (SPA)/api/*→ API REST/:code→ Redirige a URL original
Troubleshooting Deploy:
- Si falla: Revisa logs en Vercel Dashboard → Deployments
- Si BASE_URL es incorrecto: URLs generadas apuntarán a host inválido
- Si DATABASE_URL falta: App fallará al iniciar
- Si DATABASE_DRIVER es "sqlite": URLs se pierden entre requests
Los componentes van en apps/frontend/src/components/:
// Button.tsx
import React from "react";
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
loading?: boolean;
}
export const Button = ({ children, onClick, loading }: ButtonProps) => {
return (
<button
onClick={onClick}
disabled={loading}
className="px-4 py-2 bg-accent text-white rounded hover:bg-accent/90"
>
{loading ? "Cargando..." : children}
</button>
);
};-
Edita
apps/frontend/src/i18n/resources/es/common.json:{ "home": "Inicio", "contact": "Contacto" } -
Usa en componentes:
import { useTranslation } from "react-i18next"; const MyComponent = () => { const { t } = useTranslation(); return <h1>{t("home")}</h1>; };
- Crea
apps/frontend/src/i18n/resources/[idioma]/common.json - Edita
apps/frontend/src/i18n/provider.tsx - Aparecerá en el selector
El proyecto soporta drivers modulares. Para agregar PostgreSQL:
- Crea
packages/database/src/postgres-driver.ts - Implementa la interfaz
DatabaseDriver - Registra en env:
DATABASE_DRIVER=postgres
Drivers disponibles:
sqlite(default) - En memoria- PostgreSQL, MySQL (fácil de agregar)
# SQLite CLI
sqlite3 packages/database/data/dev.db ".tables"
# GUI: https://sqlitebrowser.org/Schema:
CREATE TABLE "Url" (
"id" TEXT PRIMARY KEY,
"code" TEXT UNIQUE NOT NULL,
"originalUrl" TEXT UNIQUE NOT NULL,
"qrSvg" TEXT,
"hits" INTEGER DEFAULT 0,
"createdAt" TEXT NOT NULL,
"updatedAt" TEXT NOT NULL
);
CREATE INDEX "Url_code_key" ON "Url"("code");
CREATE INDEX "Url_originalUrl_key" ON "Url"("originalUrl");# Cambiar puerto backend
PORT=3001 npm run dev:backend
# Cambiar puerto frontend (edita .env)
VITE_PORT=5174Access to XMLHttpRequest blocked by CORS policy
Solución:
- Frontend: Configura
VITE_API_BASE_URLcorrectamente - Backend: Revisa CORS en
apps/backend/src/app.ts
rm packages/database/data/dev.db
npm run devnpm cache clean --force
rm -rf node_modules package-lock.json
npm install# Reinicia el dev server
npm run devfeat: nueva funcionalidad
fix: corrección de bug
docs: actualización de documentación
style: cambios de formato
refactor: refactorización
test: tests
ci: configuración CI/CD
main → Producción
develop → Integración
feature/* → Nuevas features
fix/* → Bug fixes
- ✅ Tipado estricto obligatorio
- ❌ No usar
any - ✅ Interfaces documentadas
- ✅ Tipos compartidos en
packages/
MVP Actual: ✅ Funcional
Roadmap:
- Admin dashboard con estadísticas
- Aliases personalizados
- Autenticación de usuarios
- Expiración automática de URLs
- Rate limiting por IP
- Integración OAuth
- API key para uso programático
- Exportar QR en PNG/SVG
MIT
Las contribuciones son bienvenidas!
- Fork el proyecto
- Crea una branch:
git checkout -b feature/AmazingFeature - Commit:
git commit -m 'feat: add AmazingFeature' - Push:
git push origin feature/AmazingFeature - Abre un Pull Request
Por favor:
- Sigue las convenciones de código
- Agrega tests para nuevas funcionalidades
- Actualiza el README si es necesario
- Issues: Abre un issue en GitHub
- Email: germonramirez@gmail.com
Made with ❤️ by [German Montero]
Última actualización: 2026-04-10 | Versión: 0.1.0