| title | RestEndpoint - Strongly typed path-based HTTP API definitions |
|---|---|
| sidebar_label | RestEndpoint |
| description | Strongly typed path-based extensible HTTP API definitions. |
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import TypeScriptEditor from '@site/src/components/TypeScriptEditor'; import EndpointPlayground from '@site/src/components/HTTP/EndpointPlayground'; import Grid from '@site/src/components/Grid'; import Link from '@docusaurus/Link'; import HooksPlayground from '@site/src/components/HooksPlayground';
RestEndpoints are for HTTP based protocols like REST.
:::info extends
RestEndpoint extends Endpoint
:::
Interface
<Tabs defaultValue="RestEndpoint" values={[ { label: 'RestEndpoint', value: 'RestEndpoint' }, { label: 'Endpoint', value: 'Endpoint' }, ]}>
interface RestGenerics {
readonly path: string;
readonly schema?: Schema | undefined;
readonly method?: string;
readonly body?: any;
readonly searchParams?: any;
readonly paginationField?: string;
process?(value: any, ...args: any): any;
}
export class RestEndpoint<O extends RestGenerics = any> extends Endpoint {
/* Prepare fetch */
readonly path: string;
readonly urlPrefix: string;
readonly requestInit: RequestInit;
readonly method: string;
readonly paginationField?: string;
readonly signal: AbortSignal | undefined;
url(...args: Parameters<F>): string;
searchToString(searchParams: Record<string, any>): string;
getRequestInit(
this: any,
body?: RequestInit['body'] | Record<string, unknown>,
): Promise<RequestInit> | RequestInit;
getHeaders(headers: HeadersInit): Promise<HeadersInit> | HeadersInit;
/* Perform/process fetch */
fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response>;
parseResponse(response: Response): Promise<any>;
process(value: any, ...args: Parameters<F>): any;
testKey(key: string): boolean;
}class Endpoint<F extends (...args: any) => Promise<any>> {
constructor(fetchFunction: F, options: EndpointOptions);
key(...args: Parameters<F>): string;
readonly sideEffect?: true;
readonly schema?: Schema;
/** Default data expiry length, will fall back to NetworkManager default if not defined */
readonly dataExpiryLength?: number;
/** Default error expiry length, will fall back to NetworkManager default if not defined */
readonly errorExpiryLength?: number;
/** Poll with at least this frequency in miliseconds */
readonly pollFrequency?: number;
/** Marks cached resources as invalid if they are stale */
readonly invalidIfStale?: boolean;
/** Enables optimistic updates for this request - uses return value as assumed network response */
readonly getOptimisticResponse?: (
snap: SnapshotInterface,
...args: Parameters<F>
) => ResolveType<F>;
/** Determines whether to throw or fallback to */
readonly errorPolicy?: (error: any) => 'soft' | undefined;
testKey(key: string): boolean;
}All options are supported as arguments to the constructor, extend, and as overrides when using inheritance
const getTodo = new RestEndpoint({
path: '/todos/:id',
});const todo = await getTodo({ id: 1 });Use RestEndpoint.extend() instead of {...getTodo} (Object spread)
const updateTodo = getTodo.extend({ method: 'PUT' });export class Todo extends Entity {
id = '';
title = '';
completed = false;
}
export const getTodo = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
export const updateTodo = getTodo.extend({ method: 'PUT' });Using a Schema enables automatic data consistency without the need to hurt performance with refetching.
export class Comment extends Entity {
id = '';
title = '';
body = '';
postId = '';
static key = 'Comment';
}import { Comment } from './Comment';
const getComments = new RestEndpoint({
path: '/posts/:postId/comments',
schema: new Collection([Comment]),
searchParams: {} as { sortBy?: 'votes' | 'recent' } | undefined,
});
// Hover your mouse over 'comments' to see its type
const comments = useSuspense(getComments, {
postId: '5',
sortBy: 'votes',
});
const ctrl = useController();
const createComment = async data =>
ctrl.fetch(getComments.push, { postId: '5' }, data);schema determines the return value when used with data-binding hooks like useSuspense, useDLE, useCache or when used with Controller.fetch
export class Todo extends Entity {
id = '';
title = '';
completed = false;
static key = 'Todo';
}import { Todo } from './Todo';
const getTodo = new RestEndpoint({ path: '/', schema: Todo });
// Hover your mouse over 'todo' to see its type
const todo = useSuspense(getTodo);
async () => {
const ctrl = useController();
const todo2 = await ctrl.fetch(getTodo);
};process determines the resolution value when the endpoint is called directly. For
RestEndpoints without a schema, it also determines the return type of hooks and Controller.fetch.
interface TodoInterface {
title: string;
completed: boolean;
}
const getTodo = new RestEndpoint({
path: '/',
process(value): TodoInterface {
return value;
},
});
async () => {
// todo is TodoInterface
const todo = await getTodo();
const ctrl = useController();
const todo2 = await ctrl.fetch(getTodo);
};path used to construct the url determines the type of the first argument. If it has no patterns, then the 'first' argument is skipped.
const getRoot = new RestEndpoint({ path: '/' });
getRoot();
const getById = new RestEndpoint({ path: '/:id' });
// both number and string types work as they are serialized into strings to construct the url
getById({ id: 5 });
getById({ id: '5' });method determines whether there is a second argument to be sent as the body.
export const update = new RestEndpoint({
path: '/:id',
method: 'PUT',
});
update({ id: 5 }, { title: 'updated', completed: true });However, this is typed as 'any' so it won't catch typos.
body can be used to type the argument after the url parameters. It is only used for typing so the
value sent does not matter. undefined value can be used to 'disable' the second argument.
export const update = new RestEndpoint({
path: '/:id',
method: 'PUT',
body: {} as TodoInterface,
});
update({ id: 5 }, { title: 'updated', completed: true });
// `undefined` disables 'body' argument
const rpc = new RestEndpoint({
path: '/:id',
method: 'PUT',
body: undefined,
});
rpc({ id: 5 });searchParams can be used in a similar way to body to specify types extra parameters, used
for the GET searchParams/queryParams in a url().
const getUsers = new RestEndpoint({
path: '/:group/user/:id',
searchParams: {} as { isAdmin?: boolean; sort: 'asc' | 'desc' },
});
getList.url({ group: 'big', id: '5', sort: 'asc' }) ===
'/big/user/5?sort=asc';
getList.url({
group: 'big',
id: '5',
sort: 'desc',
isAdmin: true,
}) === '/big/user/5?isAdmin=true&sort=asc';RestEndpoint adds to Endpoint by providing customizations for a provided fetch method using inheritance or .extend().
import Lifecycle from '../diagrams/_restendpoint_lifecycle.mdx';
function fetch(...args) {
const urlParams = this.#hasBody && args.length < 2 ? {} : args[0] || {};
const body = this.#hasBody ? args[args.length - 1] : undefined;
return this.fetchResponse(
this.url(urlParams),
await this.getRequestInit(body),
)
.then(response => this.parseResponse(response))
.then(res => this.process(res, ...args));
}Members double as options (second constructor arg). While none are required, the first few have defaults.
urlPrefix + path template + '?' + searchToString(searchParams)
url() uses the params to fill in the path template. Any unused params members are then used
as searchParams (aka 'GET' params - the stuff after ?).
Implementation
import { getUrlBase, getUrlTokens } from '@rest-hooks/rest';
url(urlParams = {}) {
const urlBase = getUrlBase(this.path)(urlParams);
const tokens = getUrlTokens(this.path);
const searchParams = {};
Object.keys(urlParams).forEach(k => {
if (!tokens.has(k)) {
searchParams[k] = urlParams[k];
}
});
if (Object.keys(searchParams).length) {
return `${this.urlPrefix}${urlBase}?${this.searchToString(searchParams)}`;
}
return `${this.urlPrefix}${urlBase}`;
}Constructs the searchParams component of url.
By default uses the standard URLSearchParams global.
searchParams (aka queryParams) are sorted to maintain determinism.
Implementation
searchToString(searchParams) {
const params = new URLSearchParams(searchParams);
params.sort();
return params.toString();
}To encode complex objects in the searchParams, you can use the qs library.
import { RestEndpoint, RestGenerics } from '@data-client/rest';
import qs from 'qs';
class QSEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
searchToString(searchParams) {
// highlight-next-line
return qs.stringify(searchParams);
}
}<EndpointPlayground input="/foo?a%5Bb%5D=c" init={{method: 'GET', headers: {'Content-Type': 'application/json'}}}>
import { RestEndpoint, RestGenerics } from '@data-client/rest';
import qs from 'qs';
export default class QSEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
searchToString(searchParams) {
return qs.stringify(searchParams);
}
}import QSEndpoint from './QSEndpoint';
const getFoo = new QSEndpoint({
path: '/foo',
searchParams: {} as { a: Record<string, string> },
});
getFoo({ a: { b: 'c' } });Uses path-to-regexp v8 to build urls using the parameters passed. This also informs the types so they are properly enforced.
: prefixed words are parameter names. Both strings and numbers are accepted as values,
since they are serialized into the url string.
const getThing = new RestEndpoint({ path: '/:group/things/:id' });
getThing({ group: 'first', id: 77 });Wrap the optional segment (including its prefix) in {} to make it optional.
The type of optional parameters becomes string | number | undefined.
const optional = new RestEndpoint({
path: '/:group/things{/:number}',
});
optional({ group: 'first' });
optional({ group: 'first', number: 'fifty' });Multiple optional segments can be chained with different prefixes:
const ep = new RestEndpoint({
path: '{/:attr1}{-:attr2}{-:attr3}',
});
ep({ attr1: 'hi' });
ep({ attr2: 'hi' });
ep({ attr1: 'hi', attr3: 'ho' });*name matches one-or-more path segments. Wrap in {} to make it zero-or-more (optional).
Wildcard parameters are typed as string[] (arrays), since they represent multiple path segments.
const files = new RestEndpoint({ path: '/files/*path' });
files({ path: ['documents', 'reports', 'q4'] });
// URL: /files/documents/reports/q4
const optionalFiles = new RestEndpoint({ path: '/files{/*path}' });
optionalFiles({});
// URL: /files
optionalFiles({ path: ['documents'] });
// URL: /files/documentsParameter names must be valid JavaScript identifiers. Names containing special characters
like - or . must be quoted with double quotes:
const ep = new RestEndpoint({ path: '/:"with-dash"/:"my.param"' });
ep({ 'with-dash': 'hello', 'my.param': 'world' });Characters {}()*: and \\ are special in path-to-regexp and must be escaped with \\ when used as literals.
const getSite = new RestEndpoint({
path: 'https\\://site.com/:slug',
});
getSite({ slug: 'first' });? and + are not special in path-to-regexp v8 and do not need escaping.
This means query strings can be embedded in the path without escaping ?:
const search = new RestEndpoint({
path: '/search?{q=:q}{&page=:page}',
});
search({ q: 'test', page: 1 });
// URL: /search?q=test&page=1:::info
Types are inferred automatically from path.
Additional parameters can be specified with searchParams and body.
:::
searchParams can be to specify types extra parameters, used for the GET searchParams/queryParams in a url().
The actual value is not used in any way - this only determines typing.
<EndpointPlayground input="https://site.com/cool?isReact=true" init={{method: 'GET', headers: {'Content-Type': 'application/json'}}}>
const getReactSite = new RestEndpoint({
path: 'https\\://site.com/:slug',
searchParams: {} as { isReact: boolean },
});
getReactSite({ slug: 'cool', isReact: true });body can be used to set a second argument for mutation endpoints. The actual value is not
used in any way - this only determines typing.
This is only used by endpoings with a method that uses body: 'POST', 'PUT', 'PATCH'.
<EndpointPlayground input="https://site.com/cool" init={{method: 'POST', body: '{ "url": "/" }', headers: {'Content-Type': 'application/json'}}}>
const updateSite = new RestEndpoint({
path: 'https\\://site.com/:slug',
method: 'POST',
body: {} as { url: string },
});
updateSite({ slug: 'cool' }, { url: '/' });If specified, will add getPage method on the RestEndpoint. Pagination guide. Schema
must also contain a Collection.
Prepends this to the compiled path
export class MyEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
// this allows us to override the prefix in production environments, with a dev fallback
urlPrefix = process.env.API_SERVER ?? 'http://localhost:8000';
}Learn more about inheritance patterns for RestEndpoint
export const getTicker = new RestEndpoint({
urlPrefix: 'https://api.exchange.coinbase.com',
path: '/products/:product_id/ticker',
schema: Ticker,
});:::tip
For a dynamic prefix, try overriding the url() method instead:
const getTodo = new RestEndpoint({
path: '/todo/:id',
url(...args) {
return dynamicPrefix() + super.url(...args);
},
});:::
Method is part of the HTTP protocol.
REST protocols use these to indicate the type of operation. Because of this RestEndpoint uses this
to inform sideEffect and whether the endpoint should use a body payload. Setting
sideEffect explicitly will override this behavior, allowing for non-standard API designs.
GET is 'readonly', other methods imply sideEffects.
GET and DELETE both default to no body.
:::tip How method affects function Parameters
method only influences parameters in the RestEndpoint constructor and not .extend().
This allows non-standard method-body combinations.
body will default to any. You can always set body explicitly to take full control. undefined can be used
to indicate there is no body.
(id: string, myPayload: Record<string, unknown>) => {
const standardCreate = new RestEndpoint({
path: '/:id',
method: 'POST',
});
standardCreate({ id }, myPayload);
const nonStandardEndpoint = new RestEndpoint({
path: '/:id',
method: 'POST',
body: undefined,
});
// no second 'body' argument, because body was set to 'undefined'
nonStandardEndpoint({ id });
};:::
Prepares RequestInit used in fetch. This is sent to fetchResponse
:::tip async
import { RestEndpoint, RestGenerics } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getRequestInit(body) {
return {
...(await super.getRequestInit(body)),
method: await getMethod(),
};
}
}
async function getMethod() {
return 'GET';
}:::
Called by getRequestInit to determine HTTP Headers
This is often useful for authentication
:::warning
Don't use hooks here. If you need to use hooks, try using hookifyResource
:::
:::tip async
import { RestEndpoint, RestGenerics } from '@data-client/rest';
export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async getHeaders(headers: HeadersInit) {
return {
...headers,
'Access-Token': await getAuthToken(),
};
}
}
async function getAuthToken() {
return 'example';
}:::
Performs the fetch(input, init) call. When
response.ok is not true (like 404),
will throw a NetworkError.
Takes the Response and parses via .text() or .json() depending
on 'content-type' header having 'json' (e.g., application/json).
If status is 204, resolves as null.
Override this to handle other response types like blob or arrayBuffer.
For binary responses like file downloads, override parseResponse to use response.blob().
Set schema: undefined since binary data is not normalizable. Use dataExpiryLength: 0 to
avoid caching large blobs in memory.
const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
parseResponse(response) {
return response.blob();
},
process(blob): { blob: Blob; filename: string } {
return { blob, filename: 'download' };
},
});To extract the filename from the Content-Disposition header, override both parseResponse and process:
const downloadFile = new RestEndpoint({
path: '/files/:id/download',
schema: undefined,
dataExpiryLength: 0,
async parseResponse(response) {
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition');
const filename =
disposition?.match(/filename="?(.+?)"?$/)?.[1] ?? 'download';
return { blob, filename };
},
process(value): { blob: Blob; filename: string } {
return value;
},
});See file download guide for complete usage with browser download trigger.
Perform any transforms with the parsed result. Defaults to identity function (do nothing).
:::tip
The return type of process can be used to set the return type of the endpoint fetch:
export const getTodo = new RestEndpoint({
path: '/todos/:id',
// The identity function is the default value; so we aren't changing any runtime behavior
process(value): TodoInterface {
return value;
},
});
interface TodoInterface {
id: string;
title: string;
completed: boolean;
}import { getTodo } from './getTodo';
async (id: string) => {
// hover title to see it is a string
// see TS autocomplete by deleting `.title` and retyping the `.`
const title = (await getTodo({ id })).title;
};:::
- Global data consistency and performance with DRY state: where to expect Entities
- Functions to deserialize fields
- Race condition handling
- Validation
import { Entity, RestEndpoint } from '@data-client/rest';
class User extends Entity {
id = '';
username = '';
}
const getUser = new RestEndpoint({
path: '/users/:id',
schema: User,
});Serializes the parameters. This is used to build a lookup key in global stores.
Default:
`${this.method} ${this.url(urlParams)}`;Returns true if the provided (fetch) key matches this endpoint.
This is used for mock interceptors with with <MockResolver />, Controller.expireAll(), and Controller.invalidateAll().
import EndpointLifecycle from './_EndpointLifecycle.mdx';
Can be used to further customize the endpoint definition
const getUser = new RestEndpoint({ path: '/users/:id' });
const UserDetailNormalized = getUser.extend({
schema: User,
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': getAuth(),
};
},
});These convenience accessors create new endpoints for common Collection operations.
They only work when the RestEndpoint's schema contains a Collection.
Creates a POST endpoint that places newly created Entities at the end of a Collection.
Returns a new RestEndpoint with method: 'POST' and schema: Collection.push
const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: new Collection([Todo]),
});
// POST /todos - adds new Todo to the end of the list
const newTodo = await ctrl.fetch(
getTodos.push,
{ userId: '1' },
{ title: 'Buy groceries' },
);const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
// POST /groups/five/users - adds new User to the end of the list
const newUser = await ctrl.fetch(
UserResource.getList.push,
{ group: 'five' },
{ username: 'newuser', email: 'new@example.com' },
);Creates a POST endpoint that places newly created Entities at the start of a Collection.
Returns a new RestEndpoint with method: 'POST' and schema: Collection.unshift
const getTodos = new RestEndpoint({
path: '/todos',
searchParams: {} as { userId?: string },
schema: new Collection([Todo]),
});
// POST /todos - adds new Todo to the beginning of the list
const newTodo = await ctrl.fetch(
getTodos.unshift,
{ userId: '1' },
{ title: 'Urgent task' },
);const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
// POST /groups/five/users - adds new User to the start of the list
const newUser = await ctrl.fetch(
UserResource.getList.unshift,
{ group: 'five' },
{ username: 'priorityuser', email: 'priority@example.com' },
);Creates a POST endpoint that merges Entities into a Values Collection.
Returns a new RestEndpoint with method: 'POST' and schema: Collection.assign
const getStats = new RestEndpoint({
path: '/products/stats',
schema: new Collection(new Values(Stats)),
});
// POST /products/stats - add/update entries in the Values collection
await ctrl.fetch(getStats.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
'ETH-USD': { product_id: 'ETH-USD', volume: 500 },
});const StatsResource = resource({
urlPrefix: 'https://api.exchange.example.com',
path: '/products/:product_id/stats',
schema: Stats,
}).extend({
getList: {
path: '/products/stats',
schema: new Collection(new Values(Stats)),
},
});
// POST /products/stats - add/update entries
await ctrl.fetch(StatsResource.getList.assign, {
'BTC-USD': { product_id: 'BTC-USD', volume: 1000 },
});Creates a PATCH endpoint that removes Entities from a Collection and updates them with the response.
Returns a new RestEndpoint with method: 'PATCH' and schema: Collection.remove
const getTodos = new RestEndpoint({
path: '/todos',
schema: new Collection([Todo]),
});
// PATCH /todos - removes Todo from collection AND updates the entity
await ctrl.fetch(getTodos.remove, {}, { id: '123', completed: true });const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
// PATCH /groups/five/users - removes user from 'five' group list
// AND updates the user entity with response data (e.g., new group)
await ctrl.fetch(
UserResource.getList.remove,
{ group: 'five' },
{ id: '2', group: 'newgroup' },
);To use the remove schema with a different endpoint (e.g., DELETE):
const deleteAndRemove = MyResource.delete.extend({
schema: MyResource.getList.schema.remove,
});Creates a PATCH endpoint that moves Entities between Collections. It removes from collections matching the entity's existing state and adds to collections matching the new values (from the body/last arg).
Returns a new RestEndpoint with method: 'PATCH' and schema: Collection.move
import { kanbanFixtures, getInitialInterceptorData } from '@site/src/fixtures/kanban';
import { Entity, resource } from '@data-client/rest';
export class Task extends Entity {
id = '';
title = '';
status = 'backlog';
pk() { return this.id; }
static key = 'Task';
}
export const TaskResource = resource({
path: '/tasks/:id',
searchParams: {} as { status: string },
schema: Task,
optimistic: true,
});import { useController } from '@data-client/react';
import { TaskResource, type Task } from './TaskResource';
export default function TaskCard({ task }: { task: Task }) {
const handleMove = () => ctrl.fetch(
TaskResource.getList.move,
{ id: task.id },
{ id: task.id, status: task.status === 'backlog' ? 'in-progress' : 'backlog' },
);
const ctrl = useController();
return (
<div className="listItem">
<span style={{ flex: 1 }}>{task.title}</span>
<button onClick={handleMove}>
{task.status === 'backlog' ? '\u25bc' : '\u25b2'}
</button>
</div>
);
}import { useSuspense } from '@data-client/react';
import { TaskResource } from './TaskResource';
import TaskCard from './TaskCard';
function TaskBoard() {
const backlog = useSuspense(TaskResource.getList, { status: 'backlog' });
const inProgress = useSuspense(TaskResource.getList, { status: 'in-progress' });
return (
<div>
<div className="boardColumn">
<h4>Backlog</h4>
{backlog.map(task => <TaskCard key={task.pk()} task={task} />)}
</div>
<div className="boardColumn">
<h4>Active</h4>
{inProgress.map(task => <TaskCard key={task.pk()} task={task} />)}
</div>
</div>
);
}
render(<TaskBoard />);The remove filter is based on the entity's existing values in the store. The add filter is based on the merged entity values (existing + body). This uses the same createCollectionFilter logic as push/remove.
const UserResource = resource({
path: '/groups/:group/users/:id',
schema: User,
});
// PATCH /groups/five/users/5 - moves user 5 from 'five' group to 'ten' group
await ctrl.fetch(
UserResource.getList.move,
{ group: 'five', id: '2' },
{ id: '2', group: 'ten' },
);An endpoint to retrieve the next page using paginationField as the searchParameter key. Schema must also contain a Collection
const getTodos = new RestEndpoint({
path: '/todos',
schema: Todo,
paginationField: 'page',
});
const todos = useSuspense(getTodos);
return (
<PaginatedList
items={todos}
fetchNextPage={() =>
// fetches url `/todos?page=${nextPage}`
ctrl.fetch(TodoResource.getList.getPage, { page: nextPage })
}
/>
);See pagination guide for more info.
Creates a new endpoint with an extra paginationfield string that will be used to find the specific
page, to append to this endpoint. See Infinite Scrolling Pagination for more info.
const getNextPage = getList.paginated('cursor');Schema must also contain a Collection
function paginated<E, A extends any[]>(
this: E,
removeCursor: (...args: A) => readonly [...Parameters<E>],
): PaginationEndpoint<E, A>;The function form allows any argument processing. This is the equivalent of sending cursor string like above.
const getNextPage = getList.paginated(
({ cursor, ...rest }: { cursor: string | number }) =>
(Object.keys(rest).length ? [rest] : []) as any,
);removeCusor is a function that takes the arguments sent in fetch of getNextPage and returns
the arguments to update getList.
Schema must also contain a Collection
Make sure you use RestGenerics to keep types working.
import { RestEndpoint, type RestGenerics } from '@data-client/rest';
class GithubEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
urlPrefix = 'https://api.github.com';
getHeaders(headers: HeadersInit): HeadersInit {
return {
...headers,
'Access-Token': getAuth(),
};
}
}