Endpoint
SourceDescribe an REST API endpoint that can then be executed.
- Accepts a UrlPattern to define the URL used. Any arguments & query parameters can be passed at execution time
- Accepts a
decodeBody
function that decodes theResponse
body as returned from fetch. The defaultdecodeBody
will interpret the response based on the content type- If type includes 'json' (eg. application/json) returns decoded json
- If type includes 'text (eg. text/plain, text/html) returns text
- If status is 204 or 205 will return null
- middleware can be passed to transform the request before it is passed to
fetch
and/or the response after it has passed throughdecodeBody
. - All options accepted by
fetch
and these will be used as defaults to any call toexecute
orprepare
.
Usage:
const userList = new Action(new UrlPattern('/api/users/'));const users = await userList.execute();
You can pass urlArgs
and query
to resolve the URL:
const userDetail = new Action(new UrlPattern('/api/user/:id/'));// Resolves to /api/user/1/?showAddresses=trueconst user = await userDetail.execute({ urlArgs: { id: 1 }, query: 'showAddresses': true });
You can also pass through any fetch
options to both the constructor and calls to execute
and prepare
// Always pass through Content-Type header to all calls to userDetailconst userDetail = new Action(new UrlPattern('/api/user/:id/'), {'Content-Type': 'application/json'});// Set other fetch options at execution timeuserDetail.execute({ urlArgs: { id: 1 }, method: 'PATCH', body: JSON.stringify({ name: 'Dave' }) });
Often you have some global options you want to apply everywhere. This can be set on Endpoint
directly:
// Set default options to pass through to the request init option of `fetch`Endpoint.defaultConfig.requestInit = {headers: {'X-CSRFToken': getCsrfToken(),},};// All actions will now use the default headers specifieduserDetail.execute({ urlArgs: { id: 1 } });
You can also "prepare" an action for execution by calling the prepare
method. Each call to prepare will
return the same object (ie. it passes strict equality checks) given the same parameters. This is useful when
you need to have a stable cache key for an action. For example you may have a React hook that executes
your action when things change:
import useSWR from 'swr';...// prepare the action and pass it to useSWR. useSWR will then call the second parameter (the "fetcher")// which executes the prepared action.const { data } = useSWR([action.prepare()], (preparedAction) => preparedAction.execute());
You can wrap this up in a custom hook to make usage more ergonomic:
import { useCallback } from 'react';import useSWR from 'swr';// Wrapper around useSWR for use with `Endpoint`// @param action Endpoint to execute. Can be null if not yet ready to execute// @param args Any args to pass through to `prepare`// @return Object Same values as returned by useSWR with the addition of `execute` which// can be used to execute the action directly, optionally with new arguments.export default function useEndpoint(action, args) {const preparedAction = action ? action.prepare(args) : null;const execute = useCallback(init => preparedAction.execute(init), [preparedAction]);return {execute,...useSWR(preparedAction && [preparedAction], act => act.execute()),};}
Pagination
Pagination for an endpoint is handled by paginationMiddleware. This middleware
will add a getPaginatorClass
method to the Endpoint
which makes it compatible with usePaginator.
The default implementation chooses a paginator based on the shape of the response (eg. if the response looks like
cursor based paginator it will use CursorPaginator
, if page number based PageNumberPaginator
or if limit/offset
use LimitOffsetPaginator
- see InferredPaginator. The pagination state as returned by the
backend is stored on the instance of the paginator:
const paginator = usePaginator(endpoint);// This returns the page of resultsconst results = await endpoint.execute({ paginator });// This now has the total number of records (eg. if the paginator was PageNumberPaginator)paginator.total
You can calculate the next request state by mutating the paginator:
paginator.next()// The call to endpoint here will include the modified page request data, eg. ?page=2const results = await endpoint.execute({ paginator });
See usePaginator for more details about how to use a paginator in React.
If your backend returns data in a different shape or uses headers instead of putting details in the response body
you can handle this by a) implementing your own paginator that extends one of the base classes and customising
the getPaginationState
function or b) passing the getPaginationState
method to usePaginator.
This function can return the data in the shape expected by the paginator.
Middleware
Middleware functions can be provided to alter the url
or fetch options and transform the
response in some way.
Middleware can be defined as either an object or as a function that is passed the url, the fetch options, the next
middleware function and a context object. The function can then make changes to the url
or requestInit
and pass
it through to the next middleware function. The call to next
returns a Promise
that resolves to the response of
the endpoint after it's been processed by any middleware further down the chain. You can return a modified response here.
This middleware sets a custom header on a request but does nothing with the response:
function clientHeaderMiddleware(next, urlConfig, requestInit, context) {requestInit.headers.set('X-ClientId', 'ABC123');// Return response unmodifiedreturn next(url.toUpperCase(), requestInit)}
This middleware just transforms the response - converting it to uppercase.
function upperCaseResponseMiddleware(next, urlConfig, requestInit, context) {const { result } = await next(url.toUpperCase(), requestInit)return result.toUpperCase();}
Note that next
will return an object containing url
, response
, decodedBody
and result
.
As a convenience you can return this object directly when you do not need to modify the result
in any way (the first example above). result
contains the value returned from any middleware
that handled the response before this one or otherwise decodedBody
for the first middleware.
The context object can be used to retrieve the original options from the Endpoint.execute call and re-execute the command. This is useful for middleware that may replay a request after an initial failure, eg. if user isn't authenticated on initial attempt.
// Access the original parameters passed to executecontext.executeOptions// Re-execute the endpoint.context.execute()
NOTE: Calling context.execute()
will go through all the middleware again
Middleware can be set globally for all Endpoint's on the Endpoint.defaultConfig.middleware
option or individually for each Endpoint by passing the middleware
as an option when creating the endpoint.
Set globally:
Endpoint.defaultConfig.middleware = [dedupeInflightRequestsMiddleware,];
Or customise it per Endpoint:
new Endpoint('/users/', { middleware: [csrfTokenMiddleware] })
When middleware is passed to the Endpoint
it is appended to the default
middleware specified in Endpoint.defaultConfig.middleware
.
To change how middleware is combined per Endpoint
you can specify the
getMiddleware
option. This is passed the middleware for the Endpoint
and
the Endpoint
itself and should return an array of middleware to use.
The default implementation looks like
(middleware) => [...Endpoint.defaultConfig.middleware,...middleware,]
You can change the default implementation on Endpoint.defaultConfig.getMiddleware
Middleware can also be defined as an object with any of the following properties:
init
- Called when theEndpoint
is initialised and allows the middleware to modify the endpoint class or otherwise do some kind of initialisation.prepare
- A function that is called inEndpoint.prepare
to modify the options used. Specifically this allows middleware to apply its changes to the options used (eg. change URL etc) such thatEndpoint
correctly caches the call.process
- Process the middleware. This behaves the same as the function form described above.
Advanced
For advanced use cases there's some additional hooks available.
- addFetchStartListener - This allows middleware to be notified
when the call to
fetch
starts and get access to thePromise
. dedupeInFlightRequestsMiddleware uses this to cache an in flight request and return the same response for duplicate calls using SkipToResponse - SkipToResponse - This allows middleware to skip the rest of the middleware chain and the call
to
fetch
. Instead it is passed a promise that resolves toResponse
and this is used instead of doing the call tofetch
for you.
API
Constructor
new Endpoint(urlPattern,options)
SourceParameter | Type | Description | |
---|---|---|---|
* | urlPattern | UrlPattern | The UrlPattern to use to resolve the URL for this endpoint |
* | options | Any options accepted by fetch in addition to those described below | |
options.headers | HeadersInit|Record | Any headers to add to the request. You can unset default headers that might be specified in the default
| |
options.paginator | PaginatorInterface|null | The paginator instance to use. This can be provided in the constructor to use by default for all executions of this endpoint or provided for each call to the endpoint. Only applicable if paginationMiddleware has been added to the Endpoint. | |
options.baseUrl | string | Base URL to use. This is prepended to the return value of If not specified defaults to Endpoint.defaultConfig.baseUrl | |
options.decodeBody | Function | Method to decode body based on response. The default implementation looks at the content type of the
response and processes it accordingly (eg. handles JSON and text responses) and is suitable for most cases.
If you just need to transform the decoded body (eg. change the decoded JSON object) then use | |
options.getMiddleware | Function | Get the final middleware to apply for this endpoint. This combines the global middleware and the middleware specific to this endpoint. Defaults to Endpoint.defaultConfig.getMiddleware which applies the global middleware followed by the endpoint specific middleware. See middleware for more details | |
options.middleware | Middleware[] | Middleware to apply for this endpoint. By default See middleware for more details | |
options.resolveUrl | Function | A function to resolve the URL. It is passed the URL pattern object, any arguments for the URL and any query string parameters.
If not provided defaults to:
|
Methods
execute(options)
SourceTriggers the fetch
call for an action
This can be called directly or indirectly via prepare
.
If the fetch call itself fails due to a network error then a TypeError
will be thrown.
If the fetch call is aborted due to a call to AbortController.abort
an AbortError
is thrown.
If the response is a non-2XX response an ApiError
will be thrown.
If the call is successful the body will be decoded using decodeBody
. The default implementation
will decode JSON to an object or return text based on the content type. If the content type is
not JSON or text the raw Response
will be returned.
// Via prepareconst preparedAction = action.prepare({ urlArgs: { id: '1' }});preparedAction.execute();// Directlyaction.execute({ urlArgs: { id: '1' }});
Parameter | Type | Description | |
---|---|---|---|
* | options | Any options accepted by fetch in addition to those described below | |
options.headers | HeadersInit|Record | Any headers to add to the request. You can unset default headers that might be specified in the default
| |
options.paginator | PaginatorInterface|null | The paginator instance to use. This can be provided in the constructor to use by default for all executions of this endpoint or provided for each call to the endpoint. Only applicable if paginationMiddleware has been added to the Endpoint. | |
options.query | Query | ||
options.urlArgs | Record |
Key | Type | Description | |
---|---|---|---|
* | decodedBody | any | The value returned by |
* | query | Query | Any query string parameters |
* | requestInit | ExecuteInitOptions | The options used to execute the endpoint with |
* | response | Response | The response as returned by fetch |
* | result | T | The value returned from the endpoint after it has passed through |
* | url | string | The url that the endpoint was called with |
* | urlArgs | Record | Any arguments that were used to resolve the URL. |
prepare(options)
SourcePrepare an action for execution. Given the same parameters returns the same object. This is useful
when using libraries like useSWR
that accept a parameter that identifies a request and is used
for caching but execution is handled by a separate function.
For example to use with useSWR
you can do:
const { data } = useSWR([action.prepare()], (preparedAction) => preparedAction.execute());
If you just want to call the action directly then you can bypass prepare
and just call execute
directly.
Parameter | Type | Description | |
---|---|---|---|
* | options | Any options accepted by fetch in addition to those described below | |
options.headers | HeadersInit|Record | Any headers to add to the request. You can unset default headers that might be specified in the default
| |
options.paginator | PaginatorInterface|null | The paginator instance to use. This can be provided in the constructor to use by default for all executions of this endpoint or provided for each call to the endpoint. Only applicable if paginationMiddleware has been added to the Endpoint. | |
options.query | Query | ||
options.urlArgs | Record |
Properties
baseUrl: string
The base URL to use for this endpoint. This is prepended to the URL returned from urlPattern.resolve
.
If not specified then it defaults to Endpoint.defaultConfig.baseUrl.
Note that middleware can override this as well.
decodeBody: Function
middleware: Middleware[]
requestInit: ExecuteInitOptions
resolveUrl: Function
urlPattern: UrlPattern
The UrlPattern this endpoint hits when executed.
Static Properties
defaultConfig: DefaultConfig
Key | Type | Description | |
---|---|---|---|
* | baseUrl | string | Base to use for all urls. This can be used to change all URL's to be on a different origin. This can also be customised by middleware by changing |
* | getMiddleware | Function | Get the final middleware to apply to the specified endpoint. By default applies the global middleware followed by the endpoint specific middleware. |
* | middleware | Middleware[] | Default middleware to use on an endpoint. It is strongly recommended to append to this rather than replace it. Defaults to requestDefaultsMiddleware. See middleware for more details |
* | requestInit | RequestInit | Default options used to execute the endpoint with |
This defines the default settings to use on an endpoint globally.
All these options can be customised on individual Endpoints.