Preskočiť na obsah

Príklad: verzie API ​

GET /api/v1/users vráti name, GET /api/v2/users vráti first_name a last_name. v1 stále funguje a hovorí klientom, aby prešli na novšiu.

Otvoriť diagram v plnej veľkosti
PožiadavkaOdpoveď
/api/v1/users?id=7200 {"id":"7","name":"Ada Lovelace"} s Deprecation: true a Link na v2
/api/v2/users?id=7200 {"id":"7","first_name":"Ada","last_name":"Lovelace"}
/api/v3/users?id=7404 Unknown api endpoint
/api/v1/openapi.jsoniba endpointy v1, v1 označená ako zastaraná

Verzia 1 ​

deprecated() vráti true, čo ukáže OpenAPI. Hlavičky pridá Nette\Http\IResponse.

php
<?php

declare(strict_types=1);

namespace App\Api;

use Nette\Http\IResponse;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class UserV1Handler extends BaseHandler
{
    public function __construct(private IResponse $httpResponse)
    {
        parent::__construct();
    }

    public function summary(): string
    {
        return 'User detail, one name field';
    }

    // OpenAPI marks the endpoint deprecated: true, the console shows it crossed out
    public function deprecated(): bool
    {
        return true;
    }

    public function params(): array
    {
        return [(new GetInputParam('id'))->setRequired()];
    }

    public function handle(array $params): ResponseInterface
    {
        // deprecated() only changes the docs, clients learn about it from these headers
        $this->httpResponse->setHeader('Deprecation', 'true');
        $this->httpResponse->setHeader('Link', '</api/v2/users?id=' . rawurlencode($params['id']) . '>; rel="successor-version"');

        return new JsonApiResponse(200, ['id' => $params['id'], 'name' => 'Ada Lovelace']);
    }
}

Verzia 2 ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class UserV2Handler extends BaseHandler
{
    public function summary(): string
    {
        return 'User detail, name split in two fields';
    }

    public function params(): array
    {
        return [(new GetInputParam('id'))->setRequired()];
    }

    public function handle(array $params): ResponseInterface
    {
        // the breaking change: name is split, so it is a new version, v1 stays as it was
        return new JsonApiResponse(200, ['id' => $params['id'], 'first_name' => 'Ada', 'last_name' => 'Lovelace']);
    }
}

Služby ​

Verzia je druhý argument EndpointIdentifier.

yaml
application:
	mapping:
		Api: Tomaj\NetteApi\Presenters\*Presenter

services:
	router: App\RouterFactory::create
	apiPresenter:
		factory: Tomaj\NetteApi\Presenters\ApiPresenter
	- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
	- Tomaj\NetteApi\Error\DefaultErrorHandler
	- Tomaj\NetteApi\Link\ApiLink
	# created when /api/vN/openapi is called, it lists only the endpoints of version N
	- Tomaj\NetteApi\Handlers\OpenApiHandler(initData: [info: [title: 'Users API']])

	apiDecider:
		factory: Tomaj\NetteApi\ApiDecider
		setup:
			# the same package and action in two versions, each with its own handler
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'users'), App\Api\UserV1Handler(), Tomaj\NetteApi\Authorization\NoAuthorization())
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 2, 'users'), App\Api\UserV2Handler(), Tomaj\NetteApi\Authorization\NoAuthorization())
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'openapi'), Tomaj\NetteApi\Handlers\OpenApiHandler, Tomaj\NetteApi\Authorization\NoAuthorization())
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 2, 'openapi'), Tomaj\NetteApi\Handlers\OpenApiHandler, Tomaj\NetteApi\Authorization\NoAuthorization())

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: two versions of one endpoint, a deprecated v1 and OpenAPI per version.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
serve 8094

request "$BASE/api/v1/users?id=7"
expect_status 200 "v1"
expect_body '{"id":"7","name":"Ada Lovelace"}' "v1"
expect_header 'Deprecation: true' "v1"
expect_header 'Link: </api/v2/users?id=7>; rel="successor-version"' "v1"
ok "v1 still answers the old shape and points to v2"

request "$BASE/api/v2/users?id=7"
expect_status 200 "v2"
expect_body '{"id":"7","first_name":"Ada","last_name":"Lovelace"}' "v2"
no_header 'Deprecation' "v2"
ok "v2 answers the new shape"

request "$BASE/api/v3/users?id=7"
expect_status 404 "v3"
ok "a version that is not registered answers 404"

request "$BASE/api/v1/openapi"
expect_body '"deprecated":true' "openapi v1"
expect_body '"/api/v1/users"' "openapi v1"
request "$BASE/api/v2/openapi"
expect_body '"/api/v2/users"' "openapi v2"
if [[ "$BODY" == *'"deprecated"'* || "$BODY" == *'/api/v1/'* ]]; then echo "fail: openapi v2 lists a deprecated or v1 endpoint" >&2; exit 1; fi
ok "each version has its own OpenAPI, v1 marks users deprecated"

Časté otázky ​

Kde je verzia v URL? ​

Routa api/v<version>/<package>[/<apiAction>] ju dá do cesty; ApiDecider ju porovná spolu s metódou a balíkom. Pozrite si ApiDecider a endpointy.

Môže v2 použiť kód v1? ​

Áno, oba handlery môžu používať tú istú službu; tu sa líši iba tvar odpovede.

Povedia klientovi alebo gateway, že v1 je zastaraná a kde je nástupca, bez toho, aby ju rozbili.

Prečo jeden OpenAPI dokument na verziu? ​

OpenApiHandler vypíše endpointy svojej verzie. Klienti v2 nevidia v1. Pozrite si OpenAPI.

Funguje verzia ako reťazec? ​

Áno, EndpointIdentifier prijme reťazec ako '1.1' a predvolený vzor routy prijme bodku v /api/v1.1/....

Vydané pod licenciou MIT.