Flux REST API for redux infrastructure
Inspired by Redux-rest and is recommended to work with Redux.
with npm
npm install redux-api --save
with bower
bower install redux-api --save
=======
examples/isomorphic - React + Redux + React-Router + Redux-api with webpack and express + github api
###Initialization redux-api endpoint
import reduxApi, {transformers} from "redux-api";
- @description create endpoint
- @param options - configuration of rest-api endpoints
- @type: Object
- @default: {}
- @example:
Simple endpoint definitionGET /api/v1/entry
where response is Object
{
entry: "/api/v1/entry",
}
// equivalent
{
entry: {
url: "/api/v1/entry"
}
}
// equivalent
{
entry: {
url: "/api/v1/entry",
transformer: transformers.object, //it's default value
options: {} //it's default value
}
}
// equivalent
{
entry: {
url: "/api/v1/entry",
transformer: transformers.object, //it's default value
options: function(url, params) { //it's default value
return {};
}
}
}
###Configuration options
- @description: url endpoint
- @type: String
- @example:
{
entry: {
url: "/api/v1/entry"
}
}
- @description: function for rest response transformation
- @type: Function
- @default: transformers.object
- @example: It's a good idea to write custom transformer
for example you have response
{ "title": "Hello", "message": "World" }
Custom transformer
function customTransformer(data) {
data || (data = {});
return { title: (data.title || ""), message: (data.message || "")};
}
- @description: options for rest-api backend.
function(url, options)
- @type: Object | Funtions
- @default: null
- @example: if you use isomorphic-fetch backend
options: {
method: "post",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
}
// equivalent
options: function() {
return {
method: "post",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
};
}
####broadcast
- @description: list of actions which would emit after data fetching.
- @type: Array
- @default: null
- @example:
import {ACTION_ENTRY_UPDATE} from "./constants";
....
entry: {
url: "/api/v1/entry",
broadcast: [ ACTION_ENTRY_UPDATE ]
}
// in your redux reducer
function (state, action) {
switch (action.type) {
case ACTION_ENTRY_UPDATE:
return {
...state,
data: action.data // fetching data
};
default:
return state;
}
}
####virtual
- @description: if virtual is
true
this endpoint doesn't create reducer and doesn't emit redux-api actions. All data broadcasting by actions frombroadcast
list. - @type: Array
- @default: false
####prefetch
- @description: you can organize chain of calling events before the current endpoint will be executed
- @type: Array
- @default: null
- @example:
{
user: "/user/info",
profile: "/user/:name",
changeName: {
url: "/user/changename",
prefetch: [
function({actions, dispatch, getState}, cb) {
const {user: {data: {name}}} = getState();
name ? cb() : dispatch(actions.user(cb));
},
function({actions, dispatch, getState}, cb) {
const {user: {data: {name}}, profile: {data: {uuid}}} = getState();
uuid ? cb() : dispatch(actions.profile({name}, cb));
}
],
options: function(url, params, getState) {
const {user: {data: {uuid}}} = getState();
return { ...params, body: { ...params.body, uuid }};
}
}
}
####postfetch
- @description: you can organize chain of calling events after the current endpoint will be successful executed
- @type: Array
- @default: null
- @example:
{
user: "/user/info",
logout: {
url: "/user/logout",
postfetch: [
function({data, actions, dispatch, getState}) {
dispatch(actions.user.reset());
}
]
}
}
####validation (data, callback)
-
@param data - response data
type: Object
-
@param callback - you need to execute this callback function to finish data validation
type: Function
-
@example
{
test: {
url: "/api/test",
validation: (data, cb) {
// check data format
let error;
if (data instanceOf Array) {
error = "Data must be array";
}
cb(error);
}
}
}
- @description: Sometimes though, you might want named actions that go back to the same reducer. For example:
- @type: String
- @example:
import reduxApi, {transformers} from "redux-api";
const rest = reduxApi({
getUser: {
reducerName: "user"
url: "/user/1", // return a user object
}
updateUser: {
reducerName: "user"
url: "/user/1/update",
options: {
method: "post"
}
}
});
const {actions} = rest;
// In component with redux support (see example section)
const {dispatch} = this.props;
dispatch(rest.actions.getUser()); // GET "/api/v1/entry"
dispatch(rest.actions.updateUser({}, {
body: JSON.stringify({ name: "Hubot", login: "hubot"})
})); // POST "/api/v1/entry/1" with body
In the above example, both getUser, and updateUser update the same user reducer as they share the same reducerName For example used es7 javascript
####helpers
- @description: you can create custom helper function which work with this rest endpoint but with different parameters.
- @type: Object
- @example:
{
logger: "/api/logger",
test: {
url: "/api/test/:name/:id",
helpers: {
get(id, name) {
return [{id, name}], {}]
},
post(id, name, data) {
const {uuid} = this.getState().test;
const urlparams = {id, name};
const params = {body: {uuid, data}};
return [urlparams, params];
},
// complicated async logic
async() {
const {dispatch} = this;
return (cb)=> {
dispatch(rest.actions.logger((err)=> {
const args = [{id: 1, name: "admin"}];
cb(err, args);
}));
};
}
}
}
}
// using helpers
rest.actions.test.get(1, "admin");
// with callback
rest.actions.test.post(1, "admin", {msg: "Hello"}, (err)=> {
// end of action
});
rest.actions.test.async();
####use(key, value)
- @description initialize
reduxApi
with custom properties - @param key - name of property
- @param value - value of property
####list of properties ####fetch
- @description backend adapter. In curent example we use
adaptersFetch
adapter for rest backend usingfetch
API for rest isomorphic-fetch - @example
import adapterFetch from "redux-api/adapters/fetch";
const rest = reduxApi({...});
rest.use("fetch", adapterFetch(fetch));
####server
- @description - redux api is isomorphic compatible see examples/isomorphic By default
server===false
for clien-size mode. Ifserver===true
redux-api works in server-size mode. - @default false
const rest = reduxApi({...});
rest.use("server", true);
####rootUrl
- @description - root url for every endpoint. very usefull for isomorphic(universal) app. For clientsize use default rootUrl, and for backend use http://localhost:80 for example. For cliendsize for request
/api/get
will be/api/get
and for backend will behttp://localhost:80/api/get
- @example
const rest = reduxApi({...});
rest.use("rootUrl", "http://localhost:3000");
####init(adapter, isServer, rootUrl)
- @deprecated
- @description:
reduxApi
initializer returns non initialized object. You need to callinit
for initilize it. - @type: Function
- @param adapter - backend adapter. In curent example we use
adaptersFetch
adapter for rest backend usingfetch
API for rest isomorphic-fetch - @param isServer - redux api is isomorphic compatible see examples/isomorphic By default
isServer===false
for clien-size mode. IfisServer===true
redux-api works in server-size mode. - @param rootUrl - root url for every endpoint. very usefull for isomorphic(universal) app. For clientsize use default rootUrl, and for backend use http://localhost:80 for example. For cliendsize for request
/api/get
will be/api/get
and for backend will behttp://localhost:80/api/get
. - @example:
import "isomorphic-fetch";
import reduxApi from "redux-api";
import adapterFetch from "redux-api/adapters/fetch";
const rest = reduxApi({
... //config
});
rest.init(adapterFetch(fetch), false, "http://localhost:3000");
- @descritpion: list of redux actions for rest manipulations
- @type: Object
- @example:
const rest = reduxApi({
entries: "/api/v1/entry",
entry: {
url: "/api/v1/entry/:id",
options: {
method: "post"
}
}
});
// ....
const {actions} = rest;
/*
initialState for store
store = {
entries: {
loading: false, // request finish flag
sync: false, // data has loaded minimum once
data: {} // data
},
entry: { loading: false, sync: false, data: {} },
}
*/
// In component with redux support (see example section)
const {dispatch} = this.props;
dispatch(rest.actions.entries()); // GET "/api/v1/entry"
dispatch(rest.actions.entry({id: 1}, {
body: JSON.stringify({ name: "Hubot", login: "hubot"
}})); // POST "/api/v1/entry/1" with body
dispatch(rest.actions.entries.reset());
dispatch(rest.actions.entries.sync());
###Actions sub methods
- @description: this method save you from twice requests flag
sync
. ifsync===true
requst wouldn't execute. In server-side mode calls twice - @param urlparams - update url according Url schema
- @param params - add additional params to rest request
- @param callback - callback function when action ends
- @type: Function
- @example:
import {actions} from "./rest";
function onEnter(state, replaceState, callback) {
dispatch(rest.actions.entries.sync(callback));
}
- @description: Reset state of current reducer
- @type: Function
- @example:
import {actions} from "./rest";
function onLeave(state, replaceState, callback) {
dispatch(rest.actions.entries.sync(callback));
}
- @description: Pure xhr request without sending events or catching reducers.
- @type: Function
- @example:
import {actions} from "./rest";
actions.entries.request().then((data)=> {
....
});
/api/v1/user/:id
rest.actions.user({id: 1}) // /api/v1/user/1
/api/v1/user/(:id)
rest.actions.user({id: 1}) // /api/v1/user/1
/api/v1/user/(:id)
rest.actions.user({id: 1, test: 2}) // /api/v1/user/1?test=2
###Tools ####async
- @description - helps to organize chain call of actions
- @example
import reduxApi, { async } from "redux-api";
const rest = reduxApi({
test: "/api/test",
test2: "/api/test2",
test3: "/api/test3"
});
async(dispatch,
(cb)=> rest.actions.test(cb),
rest.actions.test2
).then((data)=> async(rest.actions.test3));
rest.js
import "isomorphic-fetch";
import reduxApi, {transformers} from "redux-api";
import adapterFetch from "redux-api/adapters/fetch";
export default reduxApi({
// simple edpoint description
entry: `/api/v1/entry/:id`,
// complex endpoint description
regions: {
url: `/api/v1/regions`,
// reimplement default `transformers.object`
transformer: transformers.array,
// base endpoint options `fetch(url, options)`
options: {
header: {
"Accept": "application/json"
}
}
}
}).init(adapterFetch(fetch)); // it's nessasary to point using rest backend
index.jsx
import React, {PropTypes} from "react";
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import { Provider, connect } from "react-redux";
import rest from "./rest"; //our redux-rest object
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(rest.reducers);
const store = createStoreWithMiddleware(reducer);
@connect((state)=> ({ entry: state.entry, regions: state.regions }))
class Application {
static propTypes = {
entry: PropTypes.object.isRequired,
regions: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired
}
componentDidMount() {
const {dispatch} = this.props;
// fetch `/api/v1/regions
dispatch(rest.actions.regions.sync());
//specify id for GET: /api/v1/entry/1
dispatch(rest.actions.entry({id: 1}));
}
render() {
const {entry, regions} = this.props;
const Regions = regions.data.map((item)=> <p>{ item.name }</p>)
return (
<div>
Loading regions: { regions.loading }
<Regions/>
Loading entry: {entry.loading}
<div>{{ entry.data.text }}</div>
</div>
);
}
}
React.render(
<Provider store={store}>
{ ()=> <Application /> }
</Provider>,
document.getElementById("content")
);
const rest = reduxApi({
user: "/user/1"
});
// initialState
{
user: {
sync: false, // State was update once
syncing: false, // State syncing is in progress
loading: false, // State updating is in progress
error: null, // response error
data: [] // response data
}
}