Ir al contenido

REST API Completa con Express y PostgreSQL

Crea una API REST production-ready con autenticación JWT, validación, testing y deployment

Construye una API REST production-ready con autenticación JWT, validación de datos, testing completo y deployment en la nube. Este tutorial te guiará paso a paso en la creación de un backend robusto y escalable.

ℹ️

Tiempo estimado: 4-6 horas | Nivel: Intermedio | Technologías: Node.js, Express, PostgreSQL, Prisma, JWT

Crearás una API REST completa con las siguientes características:

  • ✅ Autenticación JWT con refresh tokens
  • ✅ CRUD completo de usuarios
  • ✅ Validación de datos con Zod
  • ✅ Base de datos PostgreSQL con Prisma ORM
  • ✅ Testing unitario e integración con Vitest
  • ✅ Manejo de errores centralizado
  • ✅ Documentación de endpoints
  • ✅ Deployment en Railway
⚠️

Requisitos previos: Antes de comenzar, asegúrate de tener:

  • Node.js >= 18 instalado
  • PostgreSQL instalado o cuenta en Railway
  • Conocimientos básicos de JavaScript y HTTP
  • npm o yarn instalado
TecnologíaPropósito
Node.js 20+Runtime de JavaScript
Express 4Framework web minimalista
PrismaORM type-safe para PostgreSQL
ZodValidación de esquemas
jsonwebtokenAutenticación JWT
bcryptHashing de contraseñas
VitestTesting framework
RailwayPlataforma de deployment
rest-api/
├── src/
│ ├── config/
│ │ └── database.ts # Configuración de Prisma
│ ├── controllers/
│ │ ├── auth.controller.ts # Lógica de autenticación
│ │ └── user.controller.ts # Lógica de usuarios
│ ├── middleware/
│ │ ├── auth.middleware.ts # Verificación JWT
│ │ ├── error.middleware.ts # Manejo de errores
│ │ └── validate.middleware.ts # Validación con Zod
│ ├── models/
│ │ └── schemas.ts # Esquemas Zod
│ ├── routes/
│ │ ├── auth.routes.ts # Rutas de autenticación
│ │ └── user.routes.ts # Rutas de usuarios
│ ├── types/
│ │ └── express.d.ts # Tipos de Express
│ ├── utils/
│ │ └── jwt.ts # Utilidades JWT
│ └── server.ts # Entry point
├── tests/
│ ├── unit/ # Tests unitarios
│ └── integration/ # Tests de integración
├── prisma/
│ └── schema.prisma # Esquema de base de datos
├── .env.example # Variables de entorno ejemplo
├── package.json # Dependencias
└── tsconfig.json # Configuración TypeScript

Comenzamos creando el proyecto con TypeScript y las dependencias necesarias.

Ventana de terminal
mkdir rest-api && cd rest-api
npm init -y
Ventana de terminal
# Dependencias de producción
npm install express cors dotenv bcrypt jsonwebtoken
# Dependencias de desarrollo
npm install -D typescript @types/node @types/express @types/cors @types/bcrypt @types/jsonwebtoken prisma vitest @vitest/coverage-v8 tsx
Ventana de terminal
npx tsc --init
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
package.json
{
"name": "rest-api",
"version": "1.0.0",
"description": "REST API completa con Express y PostgreSQL",
"main": "dist/server.js",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "vitest",
"test:coverage": "vitest --coverage",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
},
"keywords": ["express", "typescript", "postgresql", "api"],
"author": "",
"license": "MIT"
}

Verificación: Ejecuta npm run build para confirmar que TypeScript compila correctamente.

Paso 2: Configuración de Prisma y PostgreSQL

Sección titulada «Paso 2: Configuración de Prisma y PostgreSQL»

Prisma es un ORM type-safe que facilita trabajar con bases de datos.

Ventana de terminal
npm install prisma @prisma/client
npx prisma init
.env.example
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/rest_api?schema=public"
# JWT
JWT_SECRET="your-super-secret-jwt-key-change-this"
JWT_REFRESH_SECRET="your-super-secret-refresh-key-change-this"
JWT_EXPIRES_IN="15m"
JWT_REFRESH_EXPIRES_IN="7d"
# Server
PORT=3000
NODE_ENV="development"
⚠️

Importante: Copia .env.example a .env y actualiza los valores con tus credenciales reales.

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
password String
name String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
refreshTokens RefreshToken[]
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
}
Ventana de terminal
npx prisma migrate dev --name init
ℹ️

¿Qué hace este comando? Crea la base de datos, las tablas y genera el cliente de Prisma type-safe.

Zod nos permite validar datos de forma type-safe.

src/models/schemas.ts
import { z } from 'zod';
// Esquema de registro
export const registerSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(8, 'La contraseña debe tener al menos 8 caracteres'),
name: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'),
});
// Esquema de login
export const loginSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(1, 'La contraseña es requerida'),
});
// Esquema de actualización de usuario
export const updateUserSchema = z.object({
name: z.string().min(2).optional(),
email: z.string().email().optional(),
}).partial();
// Esquema de refresh token
export const refreshTokenSchema = z.object({
refreshToken: z.string().min(1, 'Refresh token es requerido'),
});
// Tipos inferidos
export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type RefreshTokenInput = z.infer<typeof refreshTokenSchema>;
Ventana de terminal
npm install zod
src/config/database.ts
import { PrismaClient } from '@prisma/client';
// Singleton pattern para Prisma Client
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export default prisma;
src/server.ts
import express, { Application } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './routes/auth.routes';
import userRoutes from './routes/user.routes';
import errorHandler from './middleware/error.middleware';
dotenv.config();
const app: Application = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Rutas
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handler (debe ser último)
app.use(errorHandler);
// Iniciar servidor
app.listen(PORT, () => {
console.log(`🚀 Servidor corriendo en http://localhost:${PORT}`);
console.log(`📚 Ambiente: ${process.env.NODE_ENV}`);
});
src/utils/jwt.ts
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'refresh-secret';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m';
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '7d';
export interface TokenPayload {
userId: string;
email: string;
}
export const generateAccessToken = (payload: TokenPayload): string => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
export const generateRefreshToken = (payload: TokenPayload): string => {
return jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN });
};
export const verifyAccessToken = (token: string): TokenPayload => {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
};
export const verifyRefreshToken = (token: string): TokenPayload => {
return jwt.verify(token, JWT_REFRESH_SECRET) as TokenPayload;
};
export const calculateRefreshTokenExpiry = (): Date => {
const days = parseInt(JWT_REFRESH_EXPIRES_IN) || 7;
return new Date(Date.now() + days * 24 * 60 * 60 * 1000);
};
src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken, TokenPayload } from '../utils/jwt';
// Extender el tipo Request de Express
declare global {
namespace Express {
interface Request {
user?: TokenPayload;
}
}
}
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Token no proporcionado' });
return;
}
const token = authHeader.split(' ')[1];
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (error) {
res.status(401).json({ error: 'Token inválido o expirado' });
}
};
export const optionalAuth = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
const payload = verifyAccessToken(token);
req.user = payload;
}
next();
} catch {
next(); // Continuar sin autenticación
}
};
src/middleware/validate.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
export const validate =
(schema: AnyZodObject) =>
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
res.status(400).json({
error: 'Error de validación',
details: error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
})),
});
} else {
res.status(500).json({ error: 'Error interno del servidor' });
}
}
};
src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
}
}
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
): void => {
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
return;
}
// Error de Prisma
if (err.name === 'PrismaClientKnownRequestError') {
res.status(400).json({
error: 'Error de base de datos',
details: err.message,
});
return;
}
// Error genérico
console.error('Error no manejado:', err);
res.status(500).json({
error: 'Error interno del servidor',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import prisma from '../config/database';
import {
generateAccessToken,
generateRefreshToken,
calculateRefreshTokenExpiry,
} from '../utils/jwt';
import { AppError } from '../middleware/error.middleware';
export const register = async (req: Request, res: Response): Promise<void> => {
const { email, password, name } = req.body;
try {
// Verificar si el usuario ya existe
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
throw new AppError('El email ya está registrado', 400);
}
// Hash de contraseña
const hashedPassword = await bcrypt.hash(password, 10);
// Crear usuario
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
select: {
id: true,
email: true,
name: true,
isActive: true,
createdAt: true,
},
});
// Generar tokens
const accessToken = generateAccessToken({ userId: user.id, email: user.email });
const refreshToken = generateRefreshToken({ userId: user.id, email: user.email });
// Guardar refresh token
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: calculateRefreshTokenExpiry(),
},
});
res.status(201).json({
user,
accessToken,
refreshToken,
});
} catch (error) {
if (error instanceof AppError) {
res.status(error.statusCode).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error al registrar usuario' });
}
};
export const login = async (req: Request, res: Response): Promise<void> => {
const { email, password } = req.body;
try {
// Buscar usuario
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
throw new AppError('Credenciales inválidas', 401);
}
// Verificar contraseña
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new AppError('Credenciales inválidas', 401);
}
// Verificar si está activo
if (!user.isActive) {
throw new AppError('Usuario inactivo', 403);
}
// Generar tokens
const accessToken = generateAccessToken({ userId: user.id, email: user.email });
const refreshToken = generateRefreshToken({ userId: user.id, email: user.email });
// Guardar refresh token
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: calculateRefreshTokenExpiry(),
},
});
res.json({
user: {
id: user.id,
email: user.email,
name: user.name,
isActive: user.isActive,
},
accessToken,
refreshToken,
});
} catch (error) {
if (error instanceof AppError) {
res.status(error.statusCode).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error al iniciar sesión' });
}
};
export const refreshToken = async (req: Request, res: Response): Promise<void> => {
const { refreshToken } = req.body;
try {
// Buscar refresh token en BD
const tokenRecord = await prisma.refreshToken.findUnique({
where: { token: refreshToken },
include: { user: true },
});
if (!tokenRecord || tokenRecord.expiresAt < new Date()) {
throw new AppError('Refresh token inválido o expirado', 401);
}
if (!tokenRecord.user.isActive) {
throw new AppError('Usuario inactivo', 403);
}
// Generar nuevos tokens
const accessToken = generateAccessToken({
userId: tokenRecord.user.id,
email: tokenRecord.user.email,
});
const newRefreshToken = generateRefreshToken({
userId: tokenRecord.user.id,
email: tokenRecord.user.email,
});
// Actualizar refresh token
await prisma.refreshToken.update({
where: { id: tokenRecord.id },
data: {
token: newRefreshToken,
expiresAt: calculateRefreshTokenExpiry(),
},
});
res.json({
accessToken,
refreshToken: newRefreshToken,
});
} catch (error) {
if (error instanceof AppError) {
res.status(error.statusCode).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error al refrescar token' });
}
};
export const logout = async (req: Request, res: Response): Promise<void> => {
const { refreshToken } = req.body;
try {
await prisma.refreshToken.delete({ where: { token: refreshToken } });
res.json({ message: 'Sesión cerrada exitosamente' });
} catch {
res.json({ message: 'Sesión cerrada' }); // No fallar si el token no existe
}
};
src/controllers/user.controller.ts
import { Request, Response } from 'express';
import prisma from '../config/database';
import { AppError } from '../middleware/error.middleware';
export const getProfile = async (req: Request, res: Response): Promise<void> => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: {
id: true,
email: true,
name: true,
isActive: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
throw new AppError('Usuario no encontrado', 404);
}
res.json({ user });
} catch (error) {
if (error instanceof AppError) {
res.status(error.statusCode).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error al obtener perfil' });
}
};
export const updateProfile = async (req: Request, res: Response): Promise<void> => {
const { name, email } = req.body;
try {
// Verificar si email ya está en uso
if (email) {
const existingUser = await prisma.user.findFirst({
where: {
email,
NOT: { id: req.user!.userId },
},
});
if (existingUser) {
throw new AppError('El email ya está en uso', 400);
}
}
const user = await prisma.user.update({
where: { id: req.user!.userId },
data: { name, email },
select: {
id: true,
email: true,
name: true,
isActive: true,
updatedAt: true,
},
});
res.json({ user });
} catch (error) {
if (error instanceof AppError) {
res.status(error.statusCode).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error al actualizar perfil' });
}
};
export const deleteAccount = async (req: Request, res: Response): Promise<void> => {
try {
await prisma.user.delete({ where: { id: req.user!.userId } });
res.json({ message: 'Cuenta eliminada exitosamente' });
} catch (error) {
res.status(500).json({ error: 'Error al eliminar cuenta' });
}
};
src/routes/auth.routes.ts
import { Router } from 'express';
import * as authController from '../controllers/auth.controller';
import { validate } from '../middleware/validate.middleware';
import {
registerSchema,
loginSchema,
refreshTokenSchema,
} from '../models/schemas';
const router = Router();
// Registro
router.post('/register', validate(registerSchema), authController.register);
// Login
router.post('/login', validate(loginSchema), authController.login);
// Refresh token
router.post('/refresh', validate(refreshTokenSchema), authController.refreshToken);
// Logout
router.post('/logout', authController.logout);
export default router;
src/routes/user.routes.ts
import { Router } from 'express';
import * as userController from '../controllers/user.controller';
import { authenticate } from '../middleware/auth.middleware';
import { validate } from '../middleware/validate.middleware';
import { updateUserSchema } from '../models/schemas';
const router = Router();
// Todas las rutas requieren autenticación
router.use(authenticate);
// Perfil
router.get('/profile', userController.getProfile);
// Actualizar perfil
router.patch('/profile', validate(updateUserSchema), userController.updateProfile);
// Eliminar cuenta
router.delete('/profile', userController.deleteAccount);
export default router;
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules/', 'dist/', 'tests/'],
},
},
});
tests/unit/auth.controller.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import bcrypt from 'bcrypt';
import * as authController from '../../src/controllers/auth.controller';
import prisma from '../../src/config/database';
// Mock de Prisma
vi.mock('../../src/config/database', () => ({
default: {
user: {
findUnique: vi.fn(),
create: vi.fn(),
},
refreshToken: {
create: vi.fn(),
},
},
}));
// Mock de bcrypt
vi.mock('bcrypt', () => ({
default: {
hash: vi.fn(),
compare: vi.fn(),
},
}));
describe('Auth Controller', () => {
let mockReq: any;
let mockRes: any;
beforeEach(() => {
mockReq = {
body: {},
};
mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
};
vi.clearAllMocks();
});
describe('register', () => {
it('debería registrar un nuevo usuario', async () => {
mockReq.body = {
email: 'test@example.com',
password: 'password123',
name: 'Test User',
};
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
vi.mocked(bcrypt.hash).mockResolvedValue('hashedPassword');
vi.mocked(prisma.user.create).mockResolvedValue({
id: '1',
email: 'test@example.com',
name: 'Test User',
isActive: true,
createdAt: new Date(),
});
await authController.register(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(201);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
user: expect.objectContaining({
email: 'test@example.com',
}),
})
);
});
it('debería rechazar email duplicado', async () => {
mockReq.body = {
email: 'test@example.com',
password: 'password123',
name: 'Test User',
};
vi.mocked(prisma.user.findUnique).mockResolvedValue({
id: '1',
email: 'test@example.com',
} as any);
await authController.register(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
error: 'El email ya está registrado',
})
);
});
});
});
tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import express, { Application } from 'express';
import authRoutes from '../../src/routes/auth.routes';
import { errorHandler } from '../../src/middleware/error.middleware';
describe('Auth Integration Tests', () => {
let app: Application;
beforeAll(() => {
app = express();
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use(errorHandler);
});
describe('POST /api/auth/register', () => {
it('debería registrar un usuario', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: `test${Date.now()}@example.com`,
password: 'password123',
name: 'Test User',
})
.expect(201);
expect(response.body).toHaveProperty('user');
expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
});
it('debería rechazar email inválido', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'invalid-email',
password: 'password123',
name: 'Test User',
})
.expect(400);
expect(response.body).toHaveProperty('error');
});
});
});
ℹ️

Consejo: Para tests de integración, usa una base de datos separada o contenedores Docker para no afectar tus datos de desarrollo.

Ventana de terminal
npm run dev
Ventana de terminal
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "usuario@example.com",
"password": "password123",
"name": "Juan Pérez"
}'

Autenticación

POST /api/auth/register - Registrar nuevo usuario

POST /api/auth/login - Iniciar sesión

POST /api/auth/refresh - Renovar access token

POST /api/auth/logout - Cerrar sesión

Usuarios

GET /api/users/profile - Obtener perfil (requiere auth)

PATCH /api/users/profile - Actualizar perfil (requiere auth)

DELETE /api/users/profile - Eliminar cuenta (requiere auth)

Health

GET /health - Verificar estado del servidor

Registro exitoso:

{
"user": {
"id": "uuid-here",
"email": "usuario@example.com",
"name": "Juan Pérez",
"isActive": true,
"createdAt": "2024-01-01T00:00:00.000Z"
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Error de validación:

{
"error": "Error de validación",
"details": [
{
"field": "body.email",
"message": "Email inválido"
}
]
}
Ventana de terminal
# Build para producción
npm run build
railway.toml
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "npm start"
healthcheckPath = "/health"
healthcheckTimeout = 300
ℹ️

Opción 1: Conecta tu repositorio de GitHub a Railway y configura las variables de entorno.

Opción 2: Usa el CLI de Railway:

Ventana de terminal
# Instalar Railway CLI
npm install -g @railway/cli
# Login
railway login
# Inicializar proyecto
railway init
# Añadir PostgreSQL
railway add postgresql
# Añadir variables de entorno
railway variables set JWT_SECRET="your-production-secret"
railway variables set JWT_REFRESH_SECRET="your-production-refresh-secret"
# Deploy
railway up
Ventana de terminal
# Generar Prisma Client
railway run npx prisma generate
# Ejecutar migraciones
railway run npx prisma migrate deploy

¡Listo! Tu API estará disponible en la URL proporcionada por Railway.

Type Safety

TypeScript + Prisma + Zod para validación type-safe en todas las capas

Seguridad

Hashing de contraseñas, JWT con refresh tokens, validación de entradas

Testing

Tests unitarios e integración con Vitest, cobertura de código

Manejo de Errores

Middleware centralizado, mensajes descriptivos, logging apropiado

Organización

Estructura modular, separación de responsabilidades, código DRY

Production Ready

Variables de entorno, health checks, deployment automatizado

ProblemaSolución
Error de conexión a BDVerifica que DATABASE_URL sea correcta y PostgreSQL esté corriendo
Token inválidoAsegúrate de usar el mismo JWT_SECRET en generación y verificación
Error de CORSConfigura el middleware cors() con los orígenes permitidos
Migración fallidaElimina la BD y ejecuta npx prisma migrate reset
Tests fallanVerifica que las variables de entorno estén configuradas en el entorno de test

Ahora que tienes una API REST completa, puedes:

  1. Añadir más features

    • Reset de contraseña por email
    • Verificación de email
    • Roles y permisos
    • Sistema de archivos (avatar, etc.)
  2. Mejorar la infraestructura

    • Rate limiting con express-rate-limit
    • Logging con Winston o Pino
    • Métricas con Prometheus
    • Cache con Redis
  3. Mejorar testing

    • Tests E2E con Supertest
    • Mocking de servicios externos
    • CI/CD con GitHub Actions
  4. Documentación API

    • Swagger/OpenAPI
    • Postman Collection
    • Documentación auto-generada

El código completo de este tutorial está disponible en:

📁 Ver Código en GitHub


ℹ️

¿Preguntas? Abre un issue en GitHub o únete a nuestra comunidad de Discord.