Ir al contenido principal
Laravel, shipping fast.
Capítulo 16 · La evolución de la API

Versiones en el borde, no a través del código

Julian Beaujardin

El enfoque ingenuo pone la versión en la URL y copia el controlador para cada una: /v1/licenses, /v2/licenses, dos controladores, dos grupos de rutas, dos de todo. Parece disciplinado. Es una bifurcación a cámara lenta. A los pocos meses, un arreglo hay que aplicarlo dos veces, y alguien siempre olvida la segunda.

Resuelve la versión una vez, en el borde, y deja que un solo código se bifurque según ella donde haga falta. El Capítulo 6 hizo exactamente esto con el idioma: un middleware lee una señal de la petición, la contrasta con lo que se admite y fija un estado para el resto de la petición. Resolver la versión tiene la misma forma.

Empieza con un enum para el vocabulario:

// app/Enums/ApiVersion.php
enum ApiVersion: string
{
    case V1 = 'v1'; // la forma original de la licencia
    case V2 = 'v2'; // añade `status`

    public static function default(): self
    {
        return self::V2;
    }
}

Después, un middleware que la resuelve a partir de una cabecera:

// app/Http/Middleware/ResolveApiVersion.php
public function handle(
    Request $request,
    Closure $next,
): Response {
    $requested = (string) $request->header('Api-Version');

    $version = ApiVersion::tryFrom($requested)
        ?? ApiVersion::default();

    $request->attributes->set('api_version', $version);

    return $next($request);
}

Añádelo al grupo api en bootstrap/app.php, después de los middleware que el Capítulo 6 puso al final. Un middleware, un enum, un solo lugar que sabe qué significa «actual».

Hay una decisión ahí que merece un momento: qué recibe una petición sin cabecera. Aquí recibe la versión más nueva, lo que le va bien a una API interna con cuyos consumidores puedes hablar. Para una API pública, fija a cada consumidor en la versión que era la actual cuando se emitió su token, y guarda eso en el consumidor. Un valor por defecto que se mueve bajo los pies de gente que nunca lo pidió es un cambio incompatible con otro nombre.

Ejecutar dos versiones sin dos códigos

Con la versión resuelta en el borde, el controlador no se bifurca. No cambia en absoluto. Supón que el proveedor empieza a devolver un status para cada licencia, y que License gana una propiedad status que admite null para contenerlo, con una regla más y un argumento más en fromProvider(). A los consumidores existentes nunca se les prometió ese campo. La diferencia entre las versiones es una diferencia de forma, y la forma le pertenece al Resource:

// app/Http/Resources/LicenseResource.php
public function toArray(Request $request): array
{
    $version = $request->attributes->get('api_version');

    return [
        'key' => $this->resource->key,
        'name' => $this->resource->name,
        'domains' => $this->resource->domains,
        'created_at' => $this->resource->createdAt,
        'status' => $this->when(
            $version === ApiVersion::V2,
            fn () => $this->resource->status,
        ),
    ];
}

when() incluye la clave solo si la condición se cumple. Un consumidor de v1 recibe exactamente los cuatro campos que siempre recibió. (Documentación de Laravel: Eloquent: API Resources › Conditional Attributes.)

Hay un solo controlador. Licenses::all(), el driver y el objeto tipado existen exactamente una vez. Hay una sola ruta, sin prefijos v1 y v2 que mantener a la par, y un solo lugar donde se arregla un bug.

Cuando dos versiones difieren en más de uno o dos campos, dale a la versión nueva su propia clase de Resource y deja que el controlador elija entre ellas. Sigue siendo un controlador y una consulta. Lo que nunca haces es copiar el código que obtiene los datos.

La superficie con versiones es el Resource, porque ahí es donde difiere la forma. Todo lo que hay detrás (la llamada al proveedor, el objeto tipado, las reglas) ni sabe ni le importa qué versión preguntó.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.