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.
| Požiadavka | Odpoveď |
|---|---|
/api/v1/users?id=7 | 200 {"id":"7","name":"Ada Lovelace"} s Deprecation: true a Link na v2 |
/api/v2/users?id=7 | 200 {"id":"7","first_name":"Ada","last_name":"Lovelace"} |
/api/v3/users?id=7 | 404 Unknown api endpoint |
/api/v1/openapi.json | iba endpointy v1, v1 označená ako zastaraná |
Verzia 1
deprecated() vráti true, čo ukáže OpenAPI. Hlavičky pridá Nette\Http\IResponse.
<?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
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.
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
#!/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.
Čo robia hlavičky Deprecation a Link?
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/....