Simple HTTP REST Client
This library provides a framework for creating a REST HTTP client in TypeScript and JavaScript.
In other words, this library can be used to consume every type of API that uses the request/response pattern.
But this library should not be used to consume websockets or GraphQL: workflows involved in these cases are specific, there is no point in trying to unite them with what is being done in this library.
This library implements Simple HTTP Request Builder with fetch.
Though this library works seamlessly with Jersey module of Plume Framework, it can be easily adapted to work with any HTTP API.
This library is used by default in the project templates provided by
Create Plume React Project.
Installation
Using NPM:
npm install simple-http-rest-client
Using Yarn:
yarn add simple-http-rest-client
Main components example
To consume an API, there are 3 main steps:
- Configure the API client: this client will build a request and with the attached HTTP client. The API client will generally be the same for all endpoints of the same host. More API clients may be needed to differentiate public and authenticated parts of the whole host API catalog.
- Configure each API endpoint
- Consume the API endpoint
Configure the API client
This function be called for each API endpoint identified by a Method and a Path.
A request builder will be returned, when executed, this request will return a HttpPromise.
restRequest<T>(method: HttpMethod, path: string): HttpRequest<HttpPromise<T>> {
return createHttpFetchRequest(
baseUrl,
method,
path,
defaultJsonFetchClient,
options,
);
}
In a browser environment, the base URL can be built using the window.location
object: ${window.location.protocol}//${window.location.host}/api
Configure each API endpoint
Here the goal is to configure the specific API call to an endpoint. So the previous API client created will be used.
In the following example:
- The endpoint path is
/admin/session
to authenticate a user by verifying its credentials - This endpoint takes a JSON object as a body argument that complies to the TS type
SessionCredentials
- This endpoint returns a JSON object that complies to the TS type
SessionToken
authenticate(credentials: SessionCredentials): HttpPromise<SessionToken> {
return httpClient
.restRequest<SessionToken>(HttpMethod.POST, '/admin/session')
.jsonBody(credentials)
.execute();
}
Consume the API endpoint
Here the previous configured API endpoint call will be used to actually make the call inside the project logic.
authenticate(credentials: SessionCredentials) {
sessionApi
.authenticate(credentials)
.then((sessionToken: SessionToken) => {
console.log(`Session created ${sessionToken.webSessionToken}, should grant access to the user`);
})
.catch((httpError: HttpError) => {
console.log(`Authentication failed, error code: ${httpError.errorCode}`);
});
}
Types and main concepts
Basic types are:
On top of that, the HttpPromise is often used on a HttpResponse object to:
- Ease the usage of the Promise
- Make sure to have logs in case an error occurred even though a
catch
statement is not set
HttpRequest
It represents a request that will be executed by an HttpClient.
This is the main object defined in Simple HTTP Request Builder.
See directly the source type for advanced documentation: https://github.com/Coreoz/simple-http-request-builder/tree/master/src/lib/HttpRequest.ts
HttpResponse
This object represents the successful or failed HTTP response.
It is returned by an HttpClient and by a FetchResponseHandler.
HTTP failures are represented by an error.
See directly the source type for advanced documentation: https://github.com/Coreoz/simple-http-rest-client/tree/master/src/lib/client/HttpResponse.ts
FetchResponseHandler
Handlers are executed after a successful HTTP response is available: this means an HTTP response has been received (whichever the response statut, 200, 400 or 500...).
These handlers will:
- Validate some preconditions and if necessary return an error result
- Return a result
So a handler can:
- Either return a result (which can be a successful result or an error), in that case next handlers will not be executed
- Either return
undefined
, in that case next handlers will be executed
Expected results should be of type Promise
of HttpResponse.
Here are two samples usage:
HttpPromise
A mutable safe Promise
that ensures:
- Either a then function is provided or either an info log statement will be issued
- Either a catch function is provided or either a warning log statement will be issued
- Then and Catch functions are wrapped to ensure that if an error occurs during the execution,
an error statement is issued
See directly the source type for advanced documentation: https://github.com/Coreoz/simple-http-rest-client/tree/master/src/lib/promise/HttpPromise.ts
HttpError
Http errors can be:
- Raised by the library, in that case the work expected in a project will only be to display the correct error message to the end user if necessary
- Raised by the project that configured the library to a specific API that raises its own errors
See directly the source type for advanced documentation: https://github.com/Coreoz/simple-http-rest-client/tree/master/src/lib/client/HttpResponse.ts
Errors raised by the library
The errors handled by the library are:
- NETWORK_ERROR: It means the remote API could not be contacted, it is likely due to poor network connection on the client side
- TIMEOUT_ERROR: It means the remote API could be contact but no result has been returned after the timeout delay. It might also be due to poor network connection, but it can also be due to an API issue. The default timeout is 20 seconds, but that can be configured
- FORBIDDEN_ERROR: It means the API returned a 403 response. This error is raised by the validator
validateBasicStatusCodes
- INTERNAL_ERROR: It means a parsing error occurred: a
then
function provided to a HttpPromise.then()
function raised an error, the parsing of the JSON object raised an error, the server returned a non JSON response, etc. Since this error is related to a developer error or a backend error, we generally want to display the same generic error to an end user. If this error is not suitable for a project, it is possible to get rid of this by having customized validator and wrapping then
and catch
functions provided to HttpPromise to make sure these functions never fails or raised custom HttpError
These errors are already mapped to default messages in the Create Plume React Project templates: https://github.com/Coreoz/create-plume-react-project/blob/master/templates/front/src/i18n/translations/en.ts
Step by step custom usage
Here are the steps to use any custom API:
Create a basic API client
Start by creating a proxy to the fetchClient
method:
const apiFetchClient = <T>(httpRequest: HttpRequest<unknown>): Promise<HttpResponse<T>> => fetchClient(httpRequest);
Note that the function fetchClient
takes rest parameters/varargs of type FetchResponseHandler.
These parameters enable to customize the client. This is covered in the next following sections:
Add validators
The goal here is to verify that the result is excepted and correct.
By default, these validators are provided:
- validateBasicStatusCodes: It raises an error if the status code is 403, and it returns an empty response if the status code is 200
- jsonContentTypeValidator: It verifies the response content-type is JSON
- contentTypeValidator: It is used to create response content-type validator like
jsonContentTypeValidator
Once validators are added, our API client should look like this:
const apiFetchClient = <T>(httpRequest: HttpRequest<unknown>): Promise<HttpResponse<T>> => fetchClient(
httpRequest,
validateBasicStatusCodes, jsonContentTypeValidator,
);
Add a response mapper
The goal here is to parse the response's body and return the correct object. For example to parse a JSON content:
- The JSON body should be parsed (using
response.json()
) - Depending on the status code:
- If the status code is 2xx, the response object should be appended to the field
HttpResponse.response
- Else the response is an error, the error might be categorized, it should then be appended to the field
HttpResponse.error
- If the JSON parsing failed, an error should be to the field
HttpResponse.error
All these actions are implemented in the function toJsonResponse(response: Response, jsonErrorMapper: JsonErrorMapper = defaultJsonErrorMapper): Promise<HttpResponse<unknown>>
:
- Parameter
response
is the fetch
Response
to parse - Parameter
jsonErrorMapper
is the JsonErrorMapper
that will handle the parsed JSON object in case
the HTTP response is not successful: i.e. status code is not 2xx
So to configuring the JSON response mapper for a dedicated API should look like this:
const apiJsonErrorMapper: JsonErrorMapper = (response: Response, json: any) => {
if (response.status === 401) {
return insufficientPermissionsError;
}
if (typeof json.errorCode !== 'undefined') {
return { error: json };
}
logger.error('Unrecognized JSON error', response);
return { error: genericError };
};
Once this error mapper is configured, the response mapper function can be configured:
const toApiJsonResponse
: FetchResponseHandler = (response) => toJsonResponse(response, apiJsonErrorMapper);
Finally, the API client can be configured with the response mapper:
const apiFetchClient = <T>(httpRequest: HttpRequest<unknown>): Promise<HttpResponse<T>> => fetchClient(
httpRequest,
validateBasicStatusCodes, jsonContentTypeValidator,
toApiJsonResponse,
);
This can then be used following the main components example described above
by replacing defaultJsonFetchClient
by apiFetchClient
.
Note that if an API is using XML or any other language, the method toJsonResponse
should be rewritten to accept this specific language.
Advanced usages
Some components that can be used for specific use cases are described here.
SynchronizedHttpPromise
The use case addressed here is: How to avoid calling multiple times the same API whereas the first call has not yet been resolved.
So for example:
- Multiple components need to get a configuration value that is available through an API
- The result of the API is put in cache
- But by default, when loading the application, the API will be called multiple times because the first call did not yet resolve
The SynchronizedHttpPromise
will ensure there is only one HttpPromise running at a time.
PromiseMonitor
This class enables to check what are the status of promises that are being executed.
A use case is Server-Side-Rendering:
- After a first render
- It is interesting to watch the promises that are being executed
- And when all promises have revolved, it is guessable that all the data are ready for the application to rerender again
For more information, see the corresponding source code: https://github.com/Coreoz/simple-http-rest-client/tree/master/src/lib/promise/PromiseMonitor.ts
When used with an HttpFetchClient
, HttpPromiseMonitor
should be used instead of PromiseMonitor
.
It can then be used like that:
restRequest<T>(method: HttpMethod, path: string): HttpRequest<HttpPromise<T>> {
return createHttpFetchRequest(
this.baseUrl,
method,
path,
this.httpPromiseMonitor.makeMonitor(defaultJsonFetchClient, { method, path }),
);
}
Tree shaking
This library supports tree shaking: components from this library that are not used will not end in your final build as long as your bundler supports this feature.
For example, if the component SynchronizedHttpPromise
is not used, it will not be packaged in your final application build.
Vite is a great bundler to achieve this.