[Dev docs] Added final section to HTTP versioning tutorial (#154901)

## Summary

Adds the final section to the HTTP versioning tutorial about using the
route versioning specification.
This commit is contained in:
Jean-Louis Leysens 2023-04-18 15:39:29 +02:00 committed by GitHub
parent ebe278490f
commit 55b9fd2353
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -41,7 +41,7 @@ router.get(
);
```
#### Why is this problemetic for versioning?
#### Why is this problematic for versioning?
Whenever we perform a data migration the body of this endpoint will change for all clients. This prevents us from being able to maintain past interfaces and gracefully introduce new ones.
@ -119,7 +119,7 @@ router.post(
}
);
```
#### Why is this problemetic for versioning?
#### Why is this problematic for versioning?
This HTTP API currently accepts all numbers and strings as input which allows for unexpected inputs like negative numbers or non-URL friendly characters. This may break future migrations or integrations that assume your data will always be within certain parameters.
@ -141,7 +141,7 @@ This HTTP API currently accepts all numbers and strings as input which allows fo
Adding this validation we negate the risk of unexpected values. It is not necessary to use `@kbn/config-schema`, as long as your validation mechanism provides finer grained controls than "number" or "string".
In summary: think about the acceptable paramaters for every input your HTTP API expects.
In summary: think about the acceptable parameters for every input your HTTP API expects.
### 3. Keep interfaces as "narrow" as possible
@ -170,7 +170,7 @@ router.get(
The above code follows guidelines from steps 1 and 2, but it allows clients to specify ANY string by which to sort. This is a far "wider" API than we need for this endpoint.
#### Why is this problemetic for versioning?
#### Why is this problematic for versioning?
Without telemetry it is impossible to know what values clients might be passing through — and what type of sort behaviour they are expecting.
@ -207,9 +207,112 @@ router.get(
The changes are:
1. New input validation accepts a known set of values. This makes our HTTP API far _narrower_ and specific to our use case. It does not matter that our `sortSchema` has the same values as our persistence schema, what matters is that we created a **translation layer** between our HTTP API and our internal schema. This faclitates easily versioning this endpoint.
1. New input validation accepts a known set of values. This makes our HTTP API far _narrower_ and specific to our use case. It does not matter that our `sortSchema` has the same values as our persistence schema, what matters is that we created a **translation layer** between our HTTP API and our internal schema. This facilitates easily versioning this endpoint.
2. **Bonus point**: we use the `escapeKuery` utility to defend against KQL injection attacks.
### 4. Use the versioned API spec
### 4. Adhere to the HTTP versioning specification
_Under construction, check back here soon!_
#### Choosing the right version
##### Public endpoints
Public endpoints include any endpoint that is intended for users to directly integrate with via HTTP.
Choose a date string in the format `YYYY-MM-DD`. This date should be the date that a (group) of APIs was made available.
##### Internal endpoints
Internal endpoints are all non-public endpoints (see definition above).
If you need to maintain backwards-compatibility for an internal endpoint use a single, larger-than-zero number. Ex. `1`.
#### Use the versioned router
Core exposes a versioned router that ensures your endpoint's behaviour and formatting all conforms to the versioning specification.
```typescript
router.versioned.
.post({
access: 'public', // This endpoint is intended for a public audience
path: '/api/my-app/foo/{id?}',
options: { timeout: { payload: 60000 } },
})
.addVersion(
{
version: '2023-01-01', // The public version of this API
validate: {
request: {
query: schema.object({
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
}),
params: schema.object({
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
}),
body: schema.object({ foo: schema.string() }),
},
response: {
200: { // In development environments, this validation will run against 200 responses
body: schema.object({ foo: schema.string() }),
},
},
},
},
async (ctx, req, res) => {
await ctx.fooService.create(req.body.foo, req.params.id, req.query.name);
return res.ok({ body: { foo: req.body.foo } });
}
)
// BREAKING CHANGE: { foo: string } => { fooString: string } in response body
.addVersion(
{
version: '2023-02-01',
validate: {
request: {
query: schema.object({
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
}),
params: schema.object({
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
}),
body: schema.object({ fooString: schema.string() }),
},
response: {
200: {
body: schema.object({ fooName: schema.string() }),
},
},
},
},
async (ctx, req, res) => {
await ctx.fooService.create(req.body.fooString, req.params.id, req.query.name);
return res.ok({ body: { fooName: req.body.fooString } });
}
)
// BREAKING CHANGES: Enforce min/max length on fooString
.addVersion(
{
version: '2023-03-01',
validate: {
request: {
query: schema.object({
name: schema.maybe(schema.string({ minLength: 2, maxLength: 50 })),
}),
params: schema.object({
id: schema.maybe(schema.string({ minLength: 10, maxLength: 13 })),
}),
body: schema.object({ fooString: schema.string({ minLength: 0, maxLength: 1000 }) }),
},
response: {
200: {
body: schema.object({ fooName: schema.string() }),
},
},
},
},
async (ctx, req, res) => {
await ctx.fooService.create(req.body.fooString, req.params.id, req.query.name);
return res.ok({ body: { fooName: req.body.fooString } });
}
```
#### Additional reading
For a more details on the versioning specification see [this document](https://docs.google.com/document/d/1YpF6hXIHZaHvwNaQAxWFzexUF1nbqACTtH2IfDu0ldA/edit?usp=sharing).