redux-api-middleware
Redux middleware for calling an API.
Table of contents
Introduction
This middleware receives Redux Standard API-calling Actions (RSAAs) and dispatches Flux Standard Actions (FSAs) to the next middleware.
RSAAs are identified by the presence a [CALL_API]
property, where CALL_API
is a Symbol
defined in, and exported by redux-api-middleware
. They contain information describing an API call and three different types of FSAs, known as the request, success and failure FSAs.
A simple example
The following is a minimal RSAA action:
import { CALL_API } from `redux-api-middleware`;
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users',
method: 'GET',
types: ['REQUEST', 'SUCCESS', 'FAILURE']
}
}
Upon receiving this action, redux-api-middleware
will
-
check that it is indeed a valid RSAA action;
-
dispatch the following request FSA to the next middleware;
{ type: 'REQUEST' }
-
make a GET request to
http://www.example.com/api/users
; -
if the request is successful, dispatch the following success FSA to the next middleware;
{ type: 'SUCCESS', payload: { users: [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Doe' }, ] } }
-
if the request is unsuccessful, dispatch the following failure FSA to the next middleware.
{ type: 'FAILURE', payload: error // An ApiError object error: true }
We have tiptoed around error-handling issues here. For a thorough walkthrough of the redux-api-middleware
lifecycle, see Lifecycle below.
Installation
redux-api-middleware
is available on npm.
$ npm install redux-api-middleware --save
To use it, wrap the standard Redux store with it. Here is an example setup. For more information (for example, on how to add several middlewares), consult the Redux documentation.
configureStore.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { apiMiddleware } from 'redux-api-middleware';
import reducers from './reducers';
const reducer = combineReducers(reducers);
const createStoreWithMiddleware = applyMiddleware(apiMiddleware)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(reducer, initialState);
}
app.js
const store = configureStore(initialState);
Usage
Defining the API call
The parameters of the API call are specified by root properties of the [CALL_API]
property of an RSAA.
[CALL_API].endpoint
The URL endpoint for the API call.
It is usually a string, be it a plain old one or an ES2015 template string. It may also be a function taking the state of your Redux store as its argument, and returning such a string.
[CALL_API].method
The HTTP method for the API call.
It must be one of the strings GET
, HEAD
, POST
, PUT
, PATCH
, DELETE
or OPTIONS
, in any mixture of lowercase and uppercase letters.
[CALL_API].body
The body of the API call.
redux-api-middleware
uses isomorphic-fetch
to make the API call. [CALL_API].body
should hence be a valid body according to the the fetch specification. In most cases, this will be a JSON-encoded string or a FormData
object.
[CALL_API].headers
The HTTP headers for the API call.
It is usually an object, with the keys specifying the header names and the values containing their content. For example, you can let the server know your call contains a JSON-encoded string body in the following way.
{
[CALL_API]: {
...
headers: { 'Content-Type': 'application/json' }
...
}
}
It may also be a function taking the state of your Redux store as its argument, and returning an object of headers as above.
[CALL_API].credentials
Whether or not to send cookies with the API call.
It must be one of the following strings:
omit
is the default, and does not send any cookies;same-origin
only sends cookies for the current domain;include
always send cookies, even for cross-origin calls.
Bailing out
In some cases, the data you would like to fetch from the server may already be cached in you Redux store. Or you may decide that the current user does not have the necessary permissions to make some request.
You can tell redux-api-middleware
to not make the API call through [CALL_API].bailout
. You can set this property to a boolean value, and redux-api-middleware
will bail out if it is false
: the RSAA will die here, and no FSA will be passed on to the next middleware.
A more useful possibility is to give [CALL_API].bailout
a function. At runtime, it will be passed the state of your Redux store as its only argument, and the truthiness of its return value will be used to determine whether to make the call or not.
Lifecycle
The [CALL_API].types
property controls the output of redux-api-middleware
. The simplest form it can take is an array of length 3 consisting of string constants (or symbols), as in our example above. This results in the default behavior we now describe.
-
When
redux-api-middleware
receives an action, if first checks whether it has a[CALL_API]
property. If it does not, it was clearly not intended for processing withredux-api-middleware
, and so it is unceremoniously passed on to the next middleware. -
It is now time to validate the action against the RSAA definition. If there are any validation errors, a request FSA will be dispatched (if at all possible) with the following properties:
type
: the string constant in the first position of the[CALL_API].types
array;payload
: anInvalidRSAA
object containing a list of said validation errors;error: true
.
redux-api-middleware
will perform no further operations. In particular, no API call will be made, and the incoming RSAA will die here.
- Now that
redux-api-middleware
is sure it has received a valid RSAA, it will try making the API call. If everything is alright, a request FSA will be dispatched with the following property:
type
: the string constant in the first position of the[CALL_API].types
array.
But errors may pop up at this stage, for several reasons:
redux-api-middleware
has to call those of[CALL_API].bailout
,[CALL_API].endpoint
and[CALL_API].headers
that happen to be a function, which may throw an error;isomorphic-fetch
may throw an error: the RSAA definition is not strong enough to preclude that from happening (you may, for example, send in a[CALL_API].body
that is not valid according to the fetch specification — mind the SHOULDs in the RSAA definition);- a network failure occurs (the network is unreachable, the server responds with an error,...).
If such an error occurs, a different request FSA will be dispatched (instead of the one described above). It will contain the following properties:
- type
: the string constant in the first position of the [CALL_API].types
array;
- payload
: a RequestError
object containing an error message;
- error: true
.
- If
redux-api-middleware
receives a response from the server with a status code in the 200 range, a success FSA will dispatched with the following properties:
type
: the string constant in the second position of the[CALL_API].types
array;payload
: if theContent-Type
header of the response is set to something JSONy (see Success type descriptors below), the parsed JSON response of the server, or undefined otherwise.
If the status code of the response falls outside that 200 range, a failure FSA will dispatched instead, with the following properties:
- type
: the string constant in the third position of the [CALL_API].types
array;
- payload
: an ApiError
object containing the message `${status} - ${statusText}`
;
- error: true
.
Customizing the dispatched FSAs
It is possible to customize the output of redux-api-middleware
by replacing one or more of the string constants (or symbols) in [CALL_API].types
by a type descriptor.
A type descriptor is a plain JavaScript object that will be used as a blueprint for the dispatched FSAs. As such, types descriptors must have a type
property, intended to house the string constant or symbol specifying the type
of the resulting FSAs.
They may also have payload
and meta
properties, which may be of any type. Functions passed as payload
and meta
properties of type descriptors will be evaluated at runtime. The signature of these functions should be different depending on whether the type descriptor refers to request, success or failure FSAs — keep reading.
A noteworthy feature of redux-api-middleware
is that it accepts Promises (or function that return them) in payload
and meta
properties of type descriptors, and it will wait for them to resolve before dispatching the FSA — so no need to use anything like redux-promise
.
Request type descriptors
payload
and meta
functions will be passed the RSAA action itself and the state of your Redux store.
For example, if you want your request FSA to have the URL endpoint of the API call in its payload
property, you can model your RSAA on the following.
// Input RSAA
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users',
method: 'GET',
types: [
{
type: 'REQUEST',
payload: (action, state) => ({ endpoint: action.endpoint })
},
'SUCCESS',
'FAILURE'
]
}
}
// Output request FSA
{
type: 'REQUEST',
payload: { endpoint: 'http://www.example.com/api/users' }
}
If you do not need access to the action itself or the state of your Redux store, you may as well just use a static object. For example, if you want the meta
property to contain a fixed message saying where in your application you're making the request, you can do this.
// Input RSAA
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users',
method: 'GET',
types: [
{
type: 'REQUEST',
meta: { source: 'userList' }
},
'SUCCESS',
'FAILURE'
]
}
}
// Output request FSA
{
type: 'REQUEST',
meta: { source: 'userList' }
}
By default, request FSAs will not contain payload
and meta
properties.
Error request FSAs might need to obviate these custom settings though.
- Request FSAs resulting from invalid RSAAs (step 2 in Lifecycle above) cannot be customized.
redux-api-middleware
will try to dispatch an error request FSA, but it might not be able to (it may happen that the invalid RSAA does not contain a value that can be used as the request FSAtype
property, in which caseredux-api-middleware
will let the RSAA die silently). - Request FSAs resulting in request errors (step 3 in Lifecycle above) will honor the user-provided
meta
, but will ignore the user-providedpayload
, which is reserved for the default error object.
Success type descriptors
payload
and meta
functions will be passed the RSAA action itself, the state of your Redux store, and the raw server response.
For example, if you want to process the JSON response of the server using normalizr
, you can do it as follows.
import { Schema, arrayOf } from 'normalizr';
const schema = new Schema('user');
// Input RSAA
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users',
method: 'GET',
types: [
'REQUEST',
{
type: 'SUCCESS',
payload: (action, state, res) => {
const contentType = res.headers.get('Content-Type');
if (contentType && ~contentType.indexOf('json')) {
// Just making sure res.json() does not raise an error
return res.json().then((json) => normalize(json, { users: arrayOf(user) }));
}
}
},
'FAILURE'
]
}
}
// Output success FSA
{
type: 'SUCCESS',
payload: {
result: [1, 2],
entities: {
users: {
1: {
id: 1,
name: 'John Doe'
},
2: {
id: 2,
name: 'Jane Doe'
}
}
}
}
}
The above pattern of parsing the JSON body of the server response is probably quite common, so redux-api-middleware
exports a utility function getJSON
which allows for the above payload
function to be written as
(action, state, res) => {
getJSON(res).then((json) => normalize(json, { users: arrayOf(user) }));
}
By default, success FSAs will not contain a meta
property, while their payload
property will be evaluated from
(action, state, res) => getJSON(res)
Failure type descriptors
payload
and meta
functions will be passed the RSAA action itself, the state of your Redux store, and the raw server response — exactly as for success type descriptors. The error
property of dispatched failure FSAs will always be set to true
.
For example, if you want the status code and status message of a unsuccessful API call in the meta
property of your failure FSA, do the following.
{
[CALL_API]: {
endpoint: 'http://www.example.com/api/users/1',
method: 'GET',
types: [
'REQUEST',
'SUCCESS',
{
type: 'FAILURE',
meta: (action, state, res) => {
if (res) {
return {
status: res.status
statusText: res.statusText
};
} else {
return {
status: 'Network request failed'
}
}
}
}
]
}
}
By default, failure FSAs will not contain a meta
property, while their payload
property will be evaluated from
(action, state, res) =>
getJSON(res).then(
(json) => new ApiError(res.status, res.statusText, json)
)
Reference
Exports
The following objects are exported by redux-api-middleware
.
CALL_API
A JavaScript Symbol
whose presence as a key in an action signals that redux-api-middleware
should process said action.
apiMiddleware
The Redux middleware itself.
isRSAA(action)
A function that returns true
if action
has a [CALL_API]
property, and false
otherwise.
validateRSAA(action)
A function that validates action
against the RSAA definition, returning an array of validation errors.
isValidRSAA(action)
A function that returns true
if action
conforms to the RSAA definition, and false
otherwise. Internally, it simply checks the length of the array of validation errors returned by validateRSAA(action)
.
InvalidRSAA
An error class extending the native Error
object. Its constructor takes an array of validation errors as its only argument.
InvalidRSAA
objects have three properties:
name: 'InvalidRSAA'
;validationErrors
: the argument of the call to its constructor; andmessage: 'Invalid RSAA'
.
RequestError
An error class extending the native Error
object. Its constructor takes a string, intended to contain an error message.
RequestError
objects have two properties:
name: 'RequestError'
;message
: the argument of the call to its constructor.
ApiError
An error class extending the native Error
object. Its constructor takes three arguments:
- a status code,
- a status text, and
- a further object, intended for a possible JSON response from the server.
ApiError
objects have five properties:
name: 'ApiError'
;status
: the first argument of the call to its constructor;statusText
: the second argument of the call to its constructor;response
: to the third argument of the call to its constructor; andmessage : `${status} - ${statusText}`
.
getJSON(res)
A function taking a response object as its only argument. If the response object contains a JSONy Content-Type
, it returns a promise resolving to its JSON body. Otherwise, it returns a promise resolving to undefined.
Flux Standard Actions
For convenience, we recall here the definition of a Flux Standard Action.
An action MUST
- be a plain JavaScript object,
- have a
type
property.
An action MAY
- have an
error
property, - have a
payload
property, - have a
meta
property.
An action MUST NOT
- include properties other than
type
,payload
,error
andmeta
.
type
The type
of an action identifies to the consumer the nature of the action that has occurred. Two actions with the same type
MUST be strictly equivalent (using ===
). By convention, type
is usually a string constant or a Symbol
.
payload
The optional payload
property MAY be any type of value. It represents the payload of the action. Any information about the action that is not the type
or status of the action should be part of the payload
field.
By convention, if error
is true, the payload
SHOULD be an error object. This is akin to rejecting a Promise with an error object.
error
The optional error
property MAY be set to true
if the action represents an error.
An action whose error
is true is analogous to a rejected Promise. By convention, the payload
SHOULD be an error object.
If error
has any other value besides true
, including undefined
and null
, the action MUST NOT be interpreted as an error.
meta
The optional meta
property MAY be any type of value. It is intended for any extra information that is not part of the payload.
Redux Standard API-calling Actions
The definition of a Redux Standard API-calling Action below is the one used to validate RSAA actions. As explained in Lifecycle,
- actions without a
[CALL_API]
will be passed to the next middleware without any modifications; - actions with a
[CALL_API]
property that fail validation will result in an error request FSA.
A Redux Standard API-calling Action MUST
- be a plain JavaScript object,
- have a
[CALL_API]
property.
A Redux Standard API-calling Action MUST NOT
- include properties other than
[CALL-API]
.
[CALL_API]
The [CALL_API]
property MUST
- be a plain JavaScript Object,
- have an
endpoint
property, - have a
method
property, - have a
types
property.
The [CALL_API]
property MAY
- have a
body
property, - have a
headers
property, - have a
credentials
property, - have a
bailout
property.
The [CALL_API]
property MUST NOT
- include properties other than
endpoint
,method
,types
,body
,headers
,credentials
, andbailout
.
[CALL_API].endpoint
The [CALL_API].endpoint
property MUST be a string or a function. In the second case, the function SHOULD return a string.
[CALL_API].method
The [CALL_API].method
property MUST be one of the strings GET
, HEAD
, POST
, PUT
, PATCH
, DELETE
or OPTIONS
, in any mixture of lowercase and uppercase letters.
[CALL_API].body
The optional [CALL_API].body
property SHOULD be a valid body according to the the fetch specification.
[CALL_API].headers
The optional [CALL_API].headers
property MUST be a plain JavaScript object or a function. In the second case, the function SHOULD return a plain JavaScript object.
[CALL_API].credentials
The optional [CALL_API].credentials
property MUST be one of the strings omit
, same-origin
or include
.
[CALL_API].bailout
The optional [CALL_API].bailout
property MUST be a boolean or a function.
[CALL_API].types
The [CALL_API].types
property MUST be an array of length 3. Each element of the array MUST be a string, a Symbol
, or a type descriptor.
Type descriptors
A type descriptor MUST
- be a plain JavaScript object,
- have a
type
property, which MUST be a string or aSymbol
.
A type descriptor MAY
- have a
payload
property, which MAY be of any type, - have a
meta
property, which MAY be of any type.
A type descriptor MUST NOT
- have properties other than
type
,payload
andmeta
.
History
TODO
Tests
$ npm install && npm test
License
MIT
Acknowledgements
The code in this module was originally extracted from the real-world example in the redux repository, due to Dan Abramov. It has evolved thanks to issues filed by, and pull requests contributed by, other developers.