id | slug | title | summary | date | tags | ||||
---|---|---|---|---|---|---|---|---|---|
kibDevTutorialServerEndpoint |
/kibana-dev-docs/tutorials/registering-endpoints |
Registering and accessing an endpoint |
Learn how to register a new endpoint and access it |
2021-11-24 |
|
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.
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.
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)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
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 thepath
of the route, or the parameter validation schemashandler
- 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 parameterresponse
contains factory helpers to create the response to return from the endpoint
The following snippet demonstrate how to create a basic GET
endpoint on the /api/my_plugin/get_object
path:
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:
import { HttpStart } from 'kibana/public';
interface ResponseType {
result: string;
};
async function fetchData(http: HttpStart) {
return await http.get<ResponseType>(`/api/my_plugin/get_object`);
}
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.
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:
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}`);
}
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.
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:
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 })
});
}
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.
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:
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 },
});
}
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.
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',
},
});
}
);
}
}
The response
parameter of the handler already provides APIs for the most common HTTP response codes, such as
response.ok
for200
response.notFound
for404
response.badRequest
for400
- 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.
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 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
.
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 });
}
);
}
}
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.
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
.
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
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}`,
});
}
);
}
}