Networking
Introduction
The networking module provides a modular, layered network stack for your application.
Use cases
The networking module can be used for, amongst other things:
- Performing requests to remote services using a high-level API
- Using different clients and/or service adapters for different remote services, or even different parts of a single remote service.
- Encapsulating requests to perform into their own classes
- Applying middleware to all or certain requests, responses and/or errors.
High-level overview
You perform network requests by supplying instances of Request subclasses
to a Networker instance. The Networker looks at the supplied request and picks
the most appropriate registered NetworkerClient for that request. You can register
multiple clients at a networker to handle requests to different domains or even
subpaths on those domains. A networker client has a service adapter which it
uses to talk to the remote service at which it has been registered, converting
(parts of) requests and responses when necessary.
You can also register middelware at a networker instance to apply to requests to certain domains or subpaths on those domains. Some middleware classes are provided by bitlibs, for example for logging purposes and including authentication credentials in the request.
The networker
The networker is the heart of the networking module. The way in which you use it
is by first optionally registering one or more networker clients and/or middleware classes
and then handing it Request instances to perform.
Registering clients
You register instances of NetworkClient subclasses for either all requests performed
by that networker or only for requests to a certain domain or even subpath on a domain.
The networker hands incoming requests to the most appropriate client to perform.
If you do not register any specific client for a certain request, a generic NetworkClient
instance will be used.
The networker determines the most appropriate client by picking the one
registered closest to the request url. For example, for a request against url
http://api.example.com/users/1/ it first looks for any client registered for
subpath http://api.example.com/users/. If none is found it goes up a directory at a time
until it finds a client registered for that path. If no domain-specific client
is found at all it uses the globally registered client.
To register a client, use the registerClient method in one of the following ways:
import Networker from 'lib/bitlibs/network/Networker';
import NetworkerClient from 'lib/bitlibs/network/clients/NetworkerClient';
import JSONAPIClient from 'lib/bitlibs/network/clients/JSONAPIClient';
let networker = new Networker();
// Instantiate an API client and pass it the service root for which it will
// be used in order to be able to omit this root from request urls
let exampleRoot = "http://api.example.com/";
let exampleClient = new JSONAPIClient(exampleRoot);
networker.registerClient(exampleClient, exampleRoot);
// Register another client for requests to the user endpoints.
let usersRoot = `${exampleRoot}users/`;
let usersClient = new JSONAPIClient(usersRoot);
networker.registerClient(usersClient, usersRoot);
// Finally register a "catch-all" global networker client
let globalClient = new NetworkerClient();
networker.registerClient(globalClient);
Registering middleware
You can register middleware to be applied to either all requests or requests within a certain root. Middleware can be used to modify requests, responses and/or errors. The way in which the middleware to apply to a certain request is determined is similar to picking the networker client to use for a request, except for the fact that there can be more than one applicable middleware class for any given request. The middleware is performed in order of registration, meaning the one registered first for a certain path is also applied first.
Example:
import Networker from 'lib/bitlibs/network/Networker';
import LoggingMiddleware from 'lib/bitlibs/network/middleware/LoggingMiddleware';
import AuthenticationMiddleware from 'lib/bitlibs/network/clients/AuthenticationMiddleware';
import MyCustomMiddleware from 'src/network/middleware/MyCustomMiddleware';
let networker = new Networker();
// Register both the logging and authentication middlewares for the example domain.
let exampleRoot = "http://api.example.com/";
let loggingMiddleware = new LoggingMiddleware();
let authMiddleware = new AuthenticationMiddleware();
networker.registerMiddleware(loggingMiddleware, exampleRoot);
networker.registerMiddleware(authMiddleware, exampleRoot);
// In addition, register our own custom middleware to requests within the users endpoint root
let usersRoot = `${exampleRoot}users/`;
let customMiddleware = new MyCustomMiddleware(usersRoot);
networker.registerMiddleware(customMiddleware, usersRoot);
In this example, the following middleware is applied to requests to url
http://api.example.com/users/1/, in this order:
- MyCustomMiddleware
- LoggingMiddleware
- AuthenticationMiddleware
Performing requests
Once you have your networker instance properly setup you can start performing
requests with it. To this end simply call the perform method, passing it
the request instance and any additional options to pass to all applied middleware
classes:
import Networker from 'lib/bitlibs/network/Networker';
import APIEntityRequest from 'lib/bitlibs/network/requests/APIEntityRequest';
let networker = new Networker();
let usersRoot = "http://api.example.com/users/";
let getUserRequest = new APIEntityRequest(
usersRoot,
"get",
{id: 3}
);
try {
let response = await networker.perform(getUserRequest, {log: false});
} catch (error) {
// Error is an instance of a subclass of NetworkerError
console.log(error.getMessage());
}
In this example we use an APIEntityRequest to abstract performing requests regarding
a certain entity to a RESTful API. We only have to pass it the method to perform,
"retrieve", and the ID of the user to retrieve. We pass the log configuration
property to the networker to signal to the logging middleware that this request should
be exempted from logging. Performing this request using the networker results
in the get method being called on the client which is chosen to handle the
request, supplying it the final request url, parameters and headers.
Requests
A Request abstracts a certain type of request into its own class. When instantiating
a request you pass it the url, the method to perform against that URL and optionally any request
parameters, headers and extra options specific to that request class. When performing
a request the networker calls the prepare method of the request, which can alter
its attributes based on the given parameters. The APIEntityRequest for example uses
this to build the final URL by appending the supplied ID of the entity to fetch
to the given url. You could also use this to include a certain set of headers
for example, such that you do not have to explicitly provide them everytime
you execute this request (type).
See section Requests for additional information.
Responses
NetworkResponse (sub)classes provide a thin wrapper around native javascript Response
objects. Service adapters can augment it with additional data like metadata. It provides
several convenience methods for accessing certain parts of the response, including
the native response instance itself.
See section Responses for additional information.
Handling errors
If a request yielded an error the networker client will raise an instance of a subclass
of NetworkerError. This class encapsulates the error that occured, providing
an error code and message and also including the request which caused the error and
the response to that request, if any.
See section Errors for additional information.
Networker Clients
Networker clients are used by the networker to actually perform the requests for which they are eligible. You can instantiate a networker client for either a specific service root or for any requests in general. If you instantiate it with a certain service root as argument you are allowed to pass it relative urls for requests.
Performing requests
When performing a certain type of request method with a networker client, the instance method
on that networker with the same name will be called to handle that request.
Here method does not necessarily mean HTTP method: the APIRequest subclass also supports
the more semantically named methods retrieve, list, create and update for example.
So, when performing a request with method list, the list method
on the NetworkerClient is called to handle it.
The logic for performing requests is partitioned according to the request method in this way because it corresponds to how typical RESTful APIs are structured.
All request methods call the fetch method of the base NetworkerClient class
eventually. This method passes the request to the service adapter defined on the
client for possible transformation into the format accepted by the service against
which the request is being performed. It also passes the received response and
any errors to the service adapter to transform.
To recap, the following steps are performed when executing requests using a networker:
- A
Requestis handed to the networker'sperformmethod - Any applicable middleware is applied to the request.
- The method on the most applicable
NetworkerClientwith the same name as the request's method is called. - The client's dedicated request method calls the generic
fetchmethod. - The request is handed to the
ServiceAdapterfor possible transformation. - The request is performed
- The response, or any error that occured, is handed to the
ServiceAdapterfor possible transformation. - The response or error is returned to the networker.
- The networker applies any applicable middleware to the response/error.
- The networker returns the response/error to the caller.
Subclassing NetworkerClient
When subclassing NetworkerClient, here are the typical things to override/implement:
Supporting additional request methods
As stated, you support additional request methods by simply implementing instance
methods with the same name on your subclass. This method is automatically
called for every Request whose method attribute is equal to the instance method's
name. It should accept the url, an optional dictionary of request parameters and an optional
directory of headers as parameters.
getDefaultHeaders
This function determines the default set of headers to use for all requests performed by this networker client. These are overwritten by any explicitly provided headers.
getBaseRequestConfig
This method returns the base configuration to pass to the native javascript fetch
method for every request performed by this client. The default implementation
calls the getDefaultHeaders method for obtaining the default set of headers.
isErrorResponse
Should return whether a given response is regarded an error response. The default implementation regards all responses with status codes that do not start with the digit 2 as errors.
Requests
As explained in section Requests, a Request abstracts a certain type of request into its own class. Bitlibs provides several built-in request classes, mostly for easily performing requests against RESTful APIs.
APIRequest
The APIRequest class only adds functionality to append URLs with trailing slashes if so specified in the requests config.
APIEntityRequest
An APIEntityRequest encapsulates requests pertaining a particular instance of an entity on a RESTful API. You provide it with the root url of the endpoints regarding that entity type along with the ID of the entity to fetch, and the class will automatically build the final URL from those parameters.
APIEntityCollectionRequest
An APIEntityCollectionRequest encapsulates requests pertaining lists of a certain type of entity on a RESTful API. You provide it with the root url of the endpoints regarding that entity type along with any optional subpath relative to that root.
Responses
As stated in section Responses, a NetworkResponse provides a
thin wrapper around the native javascript Response class. There is only
one base NetworkResponse class provided by bitlibs, but service adapters can
return instances of their own custom subclass with additional properties if
required.
Metadata
The base NetworkResponse class contains a metadata field which is an instance
of ResponseMetaData. This encapsulates the metadata returned by the service,
if any, including convenience methods for obtaining this information.
Errors
Errors contain a uniquely identifiable error code, a message, the request which
caused the error and optionally the response which signified this error.
Built-in error codes are defined in file networking/errors/error_codes.js.
There are subclasses for every common HTTP error, some with their own additional fields.
Service Adapters
Service adapters translate requests and responses between the application and a certain (type of) service. By substituting one service adapter class for another you can easily interface with different services using the same application code. They hook into the functionality of the networker client on which they are defined at several different places:
Transforming request parameters
Service adapters are passed the set of request parameters before a request is performed. This can be used to alter the parameters in any way, for example by renaming certain parameters, adding new ones or removing some.
Processing raw responses
The service adapter is also responsible for returning an instance of a certain subclass of
NetworkResponse for a given native Response object. Some adapters may provide
their own subclass of NetworkResponse which contains additional data and/or
functionality.
Processing deserialized responses
The last way in which service adapters can modify a response is by transforming
a given NetworkResponse (subclass) instance. This method is only called for
non-error responses and can be used to populate the response with additional
information like metadata which is only included in non-error responses.
Middleware
Middleware processes and optionally transforms requests, responses and errors. It can also influence whether requests are performed at all, and suppress raised errors.
Processing requests
Requests are passed to middleware using the processRequest method. It receives
the request along with a dictionary of options passed to the networker's perform method
as arguments. It should return an array of two elements, being the (optionally modified)
request and option dictionary, in that order. It may also raise an error, in which
case the request is aborted.
Processing responses
The processResponse method receives the response instance to process along with the
dictionary of options passed to the networker's perform method as arguments.
It should return the response object, or raise an error to 'convert' the received
response into an error.
Processing errors
Lastly middleware can process any raised errors using the processError method.
This method receives the error and the options dictionary. Errors may be suppressed
by not reraising them.