Estrategias de versionado de APIs: cuándo y cómo versionar
Toda API evoluciona: la cuestión es si versionas por rutas, cabeceras o parámetros, y cómo publicar cambios incompatibles sin romper los clientes.

La primera versión de tu API necesitará cambiar. Los campos se renombran, las estructuras de respuesta evolucionan y los endpoints obsoletos hay que eliminarlos. La pregunta no es si versionar, sino cómo introducir cambios sin romper cada cliente que depende del contrato actual. La estrategia de versionado que elijas afecta a la estructura de tus URLs, la lógica de enrutamiento, la documentación y la carga de mantenimiento a largo plazo de soportar varias versiones.
Los tres enfoques
Versionado por ruta URL
El enfoque más explícito. La versión forma parte de la URL, visible en cada petición.
// URL path versioning — /api/v1/users, /api/v2/users
import express from "express";
const app = express();
// V1 routes
const v1Router = express.Router();
v1Router.get("/users", async (req, res) => {
const users = await getUsers();
// V1 returns flat structure
res.json(users.map(u => ({
id: u.id,
name: u.firstName + " " + u.lastName,
email: u.email,
})));
});
// V2 routes
const v2Router = express.Router();
v2Router.get("/users", async (req, res) => {
const users = await getUsers();
// V2 returns nested structure with pagination
res.json({
data: users.map(u => ({
id: u.id,
name: { first: u.firstName, last: u.lastName },
email: u.email,
createdAt: u.createdAt.toISOString(),
})),
pagination: { page: 1, perPage: 20, total: users.length },
});
});
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);Versionado por encabezados
La versión se especifica en un encabezado de la petición. Las URLs se mantienen limpias, pero la versión es invisible en la barra de direcciones del navegador y en los logs.
// ❌ Custom header that clients forget to set
// X-API-Version: 2
// ✅ Accept header with vendor media type
// Accept: application/vnd.myapi.v2+json
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
const accept = req.headers.accept ?? "";
const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+json/);
req.apiVersion = match ? parseInt(match[1], 10) : 1; // Default to v1
next();
}
app.get("/api/users", versionMiddleware, async (req, res) => {
const users = await getUsers();
if (req.apiVersion === 1) {
res.json(users.map(u => ({
id: u.id,
name: u.firstName + " " + u.lastName,
email: u.email,
})));
} else if (req.apiVersion === 2) {
res.json({
data: users.map(u => ({
id: u.id,
name: { first: u.firstName, last: u.lastName },
email: u.email,
createdAt: u.createdAt.toISOString(),
})),
pagination: { page: 1, perPage: 20, total: users.length },
});
}
});Versionado por parámetro de consulta
La versión es un parámetro de consulta. Sencillo de implementar, pero fácil de omitir por parte de los clientes.
// /api/users?version=2
app.get("/api/users", async (req, res) => {
const version = parseInt(req.query.version as string, 10) || 1;
// Route to version-specific handler
});Evitar versiones: cambios aditivos
La mejor versión es la que no existe. Muchos cambios pueden hacerse de forma aditiva sin romper los clientes existentes.
// ❌ Breaking change — renamed field
// V1: { name: "John Doe" }
// V2: { fullName: "John Doe" } // Every V1 client breaks
// ✅ Additive change — new field alongside old field
// V1: { name: "John Doe" }
// V1.1: { name: "John Doe", fullName: "John Doe" }
// Old clients still read "name", new clients can use "fullName"
interface UserResponseV1 {
id: string;
name: string; // Keep for backward compatibility
email: string;
}
interface UserResponseV1_1 extends UserResponseV1 {
fullName: string; // New field, old clients ignore it
firstName: string; // New field
lastName: string; // New field
}Estrategia de deprecación
Cuando una versión tenga que desaparecer eventualmente, ofrece a los clientes un calendario claro y un mecanismo de aviso.
// Deprecation middleware — warns clients on old versions
function deprecationMiddleware(version: number, sunsetDate: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (req.apiVersion === version) {
res.setHeader("Deprecation", "true");
res.setHeader("Sunset", sunsetDate);
res.setHeader(
"Link",
'</api/v2/docs>; rel="successor-version"'
);
// Log deprecated version usage for migration tracking
console.log(JSON.stringify({
event: "deprecated_api_call",
version,
path: req.path,
clientId: req.headers["x-client-id"],
sunsetDate,
}));
}
next();
};
}
app.use("/api/v1", deprecationMiddleware(1, "2021-06-01T00:00:00Z"));Enrutamiento de versiones con un patrón de controladores
Para APIs más grandes, un patrón de controladores limpio mantiene organizada la lógica específica de cada versión.
// ❌ Version checks scattered throughout handler code
app.get("/api/users/:id", (req, res) => {
if (req.apiVersion === 1) { /* ... */ }
else if (req.apiVersion === 2) { /* ... */ }
else if (req.apiVersion === 3) { /* ... */ }
// Unmaintainable spaghetti
});
// ✅ Version-specific controllers with shared business logic
// controllers/users/v1.ts
export class UsersControllerV1 {
async getUser(req: Request, res: Response) {
const user = await userService.findById(req.params.id);
res.json(this.serialize(user));
}
protected serialize(user: User) {
return { id: user.id, name: `${user.firstName} ${user.lastName}` };
}
}
// controllers/users/v2.ts
export class UsersControllerV2 extends UsersControllerV1 {
protected serialize(user: User) {
return {
id: user.id,
name: { first: user.firstName, last: user.lastName },
createdAt: user.createdAt.toISOString(),
};
}
}
// Registration
const v1Users = new UsersControllerV1();
const v2Users = new UsersControllerV2();
v1Router.get("/users/:id", (req, res) => v1Users.getUser(req, res));
v2Router.get("/users/:id", (req, res) => v2Users.getUser(req, res));Puntos clave
- El versionado por ruta URL es el más explícito — visible en los logs, fácil de enrutar, sencillo para los clientes
- Prefiere cambios aditivos antes que nuevas versiones — añadir campos es retrocompatible, eliminarlos o renombrarlos no lo es
- Define encabezados de deprecación y fechas de retirada — ofrece a los clientes calendarios claros y rutas de migración
- Usa herencia de controladores para la lógica específica de cada versión — mantén la lógica de negocio compartida en la base y sobrescribe la serialización por versión
- Monitoriza el uso de versiones obsoletas — sabe qué clientes siguen llamando a versiones antiguas antes de retirarlas
- Soporta como máximo dos versiones simultáneamente — mantener más de dos versiones activas genera una sobrecarga insostenible


