kibana/dev_docs/tutorials/endpoints.mdx
Kibana Machine 4975c1c444
Improve the HTTP and routing doc with more examples (#119591) (#120092)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Pierre Gayvallet <pierre.gayvallet@elastic.co>
2021-12-01 07:33:39 -07:00

438 lines
14 KiB
Text

---
id: kibDevTutorialServerEndpoint
slug: /kibana-dev-docs/tutorials/registering-endpoints
title: Registering and accessing an endpoint
summary: Learn how to register a new endpoint and access it
date: 2021-11-24
tags: ['kibana', 'dev', 'architecture', 'tutorials']
---
## The HTTP service API
### The server-side HTTP service
The server-side `HttpService` allows server-side plugins to register endpoints with built-in support for request validation. These endpoints may be used by client-side code or be exposed as a public API for users. Most plugins integrate directly with this service.
The service allows plugins to:
- extend the Kibana server with custom HTTP API.
- execute custom logic on an incoming request or server response.
- implement custom authentication and authorization strategy.
<DocCallOut>
See [the server-side HTTP service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md)
</DocCallOut>
### The client-side HTTP service
The client-side counterpart of the HTTP service provides an API to communicate with the Kibana server via HTTP interface.
The client-side `HttpService` is a preconfigured wrapper around `window.fetch` that includes some default behavior and automatically handles common errors (such as session expiration).
**The service should only be used for access to backend endpoints registered by the same plugin.** Feel free to use another HTTP client library to request 3rd party services.
<DocCallOut>
See [the client-side HTTP service API docs](https://github.com/elastic/kibana/blob/main/docs/development/core/public/kibana-plugin-core-public.httpsetup.md)
</DocCallOut>
## Registering an endpoint
Registering an endpoint, or `route`, is done during the `setup` lifecycle stage of a plugin. The first step to register a route
is to create a [router](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.irouter.md)
using the `http` core service.
Once the router is instantiated, it is possible to use its APIs, such as `router.get` or `router.post` to create a route for the equivalent
HTTP method. All these APIs share the same signature, and receive two parameters:
- `route` - the route configuration, such as the `path` of the route, or the parameter validation schemas
- `handler` - the handler function that will be called when a request matching the route configuration is received
When invoked, the `handler` receive three parameters: `context`, `request`, and `response`, and must return a response that will be sent to serve
the request.
- `context` is a request-bound context exposed for the request. It allows for example to use an elasticsearch client bound to the request's credentials.
- `request` contains information related to the request, such as the path and query parameter
- `response` contains factory helpers to create the response to return from the endpoint
<DocCallOut>
See the [request](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md)
and [response](https://github.com/elastic/kibana/blob/main/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md)
documentation
</DocCallOut>
## Basic examples
### Registering a GET endpoint
The following snippet demonstrate how to create a basic `GET` endpoint on the `/api/my_plugin/get_object` path:
```ts
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/get_object',
validate: false,
},
async (context, request, response) => {
return response.ok({
body: { result: 'everything is alright'},
});
}
);
}
}
```
consuming the endpoint from the client-side using core's `http` service would then look like:
```ts
import { HttpStart } from 'kibana/public';
interface ResponseType {
result: string;
};
async function fetchData(http: HttpStart) {
return await http.get<ResponseType>(`/api/my_plugin/get_object`);
}
```
### Using and validating path parameters
It is possible to specify dynamic parameters in the `path` of the endpoint using the `{name}` syntax.
When doing so, the associated validation schema must be defined via the `validate.params` option
of the route definition.
```ts
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/get_object/{id}',
validate: {
params: schema.object({
id: schema.string(),
})
},
},
async (context, request, response) => {
const { id } = request.params;
const data = await findObject(id);
if (!data) {
return response.notFound();
}
return response.ok({ body: data });
}
);
}
}
```
consuming the endpoint from the client-side using core's `http` service would then look like:
```ts
import { HttpStart } from 'kibana/public';
import { MyObjectType } from '../common/types';
async function fetchData(http: HttpStart, id: string) {
return await http.get<MyObjectType>(`/api/my_plugin/get_object/${id}`);
}
```
### Registering a POST endpoint and validating the payload
Similar to the validation we performed against the path parameters in the previous example, the `body` validation schema
must be provided when registering a `post` handler that will access the payload.
```ts
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.post(
{
path: '/api/my_plugin/objects/{id}/update',
validate: {
params: schema.object({
id: schema.string(),
}),
body: schema.object({
title: schema.string(),
description: schema.string()
}),
},
},
async (context, request, response) => {
const { id } = request.params;
const { title, description } = request.body;
await updateObject(id, { title, description });
return response.ok({ body: { updated: true }});
}
);
}
}
```
consuming the endpoint from the client-side using core's `http` service would then look like:
```ts
import { HttpStart } from 'kibana/public';
interface ResponseType {
updated: boolean;
};
interface UpdateOptions {
title?: string;
description?: string;
}
async function fetchData(http: HttpStart, id: string, { title, description }: UpdateOptions) {
return await http.post<ResponseType>(`/api/my_plugin/objects/${id}/update`, {
body: JSON.stringify({ title, description })
});
}
```
### Using the query parameters
Similar to the `body` validation, the query parameters schema has to be defined using the `validate.query`
option of the route definition to be accessible from the handler.
```ts
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/objects/find',
validate: {
query: schema.object({
term: schema.maybe(schema.string()),
page: schema.number({ min: 1, defaultValue: 1 }),
perPage: schema.number({ min: 5, max: 50, defaultValue: 10 }),
}),
},
},
async (context, request, response) => {
const { term, page, perPage } = request.query;
const results = await findObjects(term, { page, perPage });
return response.ok({ body: { results } });
}
);
}
}
```
consuming the endpoint from the client-side using core's `http` service would then look like:
```ts
import { HttpStart } from 'kibana/public';
import { MyObjectType } from '../common/types';
interface ResponseType {
results: MyObjectType[];
};
interface UpdateOptions {
term?: string;
page?: number;
perPage?: number;
}
async function fetchData(http: HttpStart, { term, page, perPage }: UpdateOptions) {
return await http.get<ResponseType>(`/api/my_plugin/objects/find`, {
query: { term, page, perPage },
});
}
```
## Customizing the response
### Attaching headers to the response
All APIs of the `response` parameter of the handler accept a `headers` property that can be used
to define headers to attach to the response.
```ts
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/some_text_response',
validate: false,
},
async (context, request, response) => {
return response.ok({
body: 'some plain text response',
headers: {
'content-type': 'text/plain',
'cache-control': 'must-revalidate',
},
});
}
);
}
}
```
### Defining a specific HTTP status code for the response
The `response` parameter of the handler already provides APIs for the most common HTTP response codes, such as
- `response.ok` for `200`
- `response.notFound` for `404`
- `response.badRequest` for `400`
- and so on
However, some of the less commonly used return codes don't have such helpers. In that case, the `response.custom`
and/or `response.customError` APIs should be used.
```ts
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/some_text_response',
validate: false,
},
async (context, request, response) => {
return response.custom({
body: `Kibana is a teapot`,
statusCode: 418,
});
}
);
}
}
```
## Some advanced usages
### Handling request cancellation
Some request lifecycle events are exposed to the handler via `request.events` in the form of `rxjs`
observables, such as `request.events.aborted$` that emits if/when the request is canceled by the client.
These observables can either be used directly, or be used to control an `AbortController`.
```ts
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/objects/find',
validate: {
query: schema.object({
term: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const { term } = request.query;
const { aborted$ } = request.events;
const abortController = new AbortController();
aborted$.subscribe(() => {
abortController.abort();
});
const results = await findObjects(term, { abortController });
return response.ok({ body: results });
}
);
}
}
```
### Disabling authentication for an endpoint
By default, when security is enabled, endpoints require the user to be authenticated to be accessed,
and will return a `401 - Unauthorized` otherwise.
It is possible to disable this requirement using the `authRequired` option of the route.
```ts
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/get_object',
validate: false,
options: {
authRequired: false,
},
},
async (context, request, response) => {
return response.ok({
body: { authenticated: request.auth.isAuthenticated },
});
}
);
}
}
```
Note that in addition to `true` and `false`, `authRequired` accepts a third value, `'optional'`. When used,
Kibana will try to authenticate the user but will allow access to the endpoint regardless of the result. In that
case, the developer needs to manually checks if the user is authenticated via `request.auth.isAuthenticated`.
### Accessing the url or route configuration from the handler
In some advanced use cases, such as generic handlers being used for multiple routes, it can be useful to know,
from within the handler, the route configuration and the actual url that was requested by the user. This can
be achieved by using the `url` and `route` properties of the `request` parameter of the handler.
request.url / request.route
```ts
import type { CoreSetup, Plugin } from 'kibana/server';
export class MyPlugin implements Plugin {
public setup(core: CoreSetup) {
const router = core.http.createRouter();
router.get(
{
path: '/api/my_plugin/get_object',
validate: false,
options: {
authRequired: false,
},
},
async (context, request, response) => {
const { url, route } = request;
return response.ok({
body: `You requested ${route.method} ${url}`,
});
}
);
}
}
```
## More examples
<DocCallOut>
See [the routing example plugin](https://github.com/elastic/kibana/blob/main/examples/routing_example) for more route registration examples.
</DocCallOut>