Restful.js: Review of an open-source project
Premises
If you've already played with AngularJS you probably know the famous Restangular project.
Restangular is an AngularJS service that simplifies common GET, POST, DELETE, and UPDATE requests with a minimum of client code. It's a perfect fit for any WebApp that consumes data from a RESTful API.
Restangular exposes an API which looks like the url of the resource you want to deal with:
Restangular.one("accounts", 123)
.one("buildings", 456)
.get();
Which results to:
GET /accounts/123/buildings/456
At marmelab, we wanted a framework-agnostic tool such as Restangular to call our APIs. That's why we built it at the beginning!
To learn more about it, I suggest you to read: A Framework Agnostic JS Client for RESTful APIs.
First draft
People quickly started being interested but many major issues were found. Their origin was obvious, the foundation of restful.js was bad, suffering from an hackday code. Each layer of the lib was isolated in a closure which led to unnecessary complex code. Furthermore the API exposed by restful.js wasn't understood by end users.
Also, maintaining an open-source project is hard! Especially when you stop using it in your own projects. That's why the lib was left unattended for several months. It was buggy but some workarounds were posted on the issues tracker of the GitHub project.
Let's rewrite everything
We have learned a lot with the first version:
- People started using restful.js in some Node.js applications and it worked painfully because it wasn't designed for that.
- Interceptors had a random behaviour because the HTTP backend we used was not the best for our usage. To keep control over the interceptor logic it was better to rewrite this part from scratch.
- The Developper experience was awful, and adding some new features was very difficult, even for me.
The conclusion became obvious, we had to start it again. I tried to keep as much as possible the same API but with some BC breaks when necessary.
I splitted the HTTP layer from the restful model to allow people using several HTTP backends.
For a browser usage (with fetch):
import whatwg-fetch;
import restful, { fetchBackend } from 'restful.js';
const api = restful('http://api.example.com', fetchBackend(fetch));
For a Node.js usage (with request):
import request from "request";
import restful, { requestBackend } from "restful.js";
const api = restful("http://api.example.com", requestBackend(request));
I also removed some unrelated code like the Promise polyfill. Indeed, the polyfill should be added once for all in your application and not inside a random lib. This new version fixed almost all the reported issues. The test coverage was much better and people started to make some pull requests. The main objective was completed.
Rewriting the new version was a good exercice. As always it's confirmed that tests are the key for long term projects. Since I've arrived at marmelab, it is the most valuable thing I've learned. I can't build any projects without tests anymore. I knew where I wanted to go this time, and then the code was much easier to write.
Should you use restful.js?
I said you before I didn't use restful.js anymore in my own projects or even in marmelab projects. Don't be afraid, it's not because I think it is a bad lib. I started using Redux a few months ago in our React applications. It brings the concept of express middleware to the frontend. Therefore, I prefer a lot using a fetch middleware to deal with my API calls.
Let's take a simple component which calls a redux action:
// Foo.jsx
import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import { callApi } from "./actions";
export class Foo extends Compnent {
static propTypes = {
callApiAction: PropTypes.func.isRequired,
};
constructor(props, context) {
super(props, context);
this.onClick = this.onClick.bind(this);
}
onClick(event) {
event.preventDefault();
this.props.callApiAction();
}
render() {
return <button onClick={this.onClick}>Click me</button>;
}
}
export default connect(
null,
{
callApiAction: callApi,
}
);
If we use a fetch middleware we only need to dispatch a payload with a specific pattern:
// actions.js
export const CALL_API = "CALL_API";
export function callApi() {
return {
payload: {
request: {
url: "/bar",
},
},
type: CALL_API,
};
}
At this point the usage of restul.js should be in the fetch middleware:
// fetchMiddleware.js
import whatwg-fetch;
import restful, { fetchBackend } from 'restful.js';
const api = restful('http://api.example.com', fetchBackend(fetch));
export default function fetchMiddleware() {
return next => action => {
const { request, ...payload } = action.payload;
if (!request) {
return next(action);
}
next({
payload,
type: `${action.type}_PENDING`,
});
const method = request.method || 'get';
return api.custom(request.url)[method].then(
response => next({
payload: {
...payload,
response,
},
type: `${action.type}_SUCCESS`,
}),
error => next({
payload: {
...payload,
error,
},
type: `${action.type}_FAILURE`,
})
);
};
}
As you can see there isn't any real benefit in using restful.js here over native fetch. The remaining useful feature from restful.js could be its interceptors. But as we are using redux, you can write interceptors as middlewares. Therefore, restful.js becomes useless in our case.
To conclude on restful.js usage, I would answer it is your call. Depending on your needs restful.js could be useful, or it could be a redundant and therefore useless. You should always use the right lib for your project.
Anyway the code is easily understandable now, has a lot of tests. I encourage you to send pull requests. It is much easier to find to time to review a pull request than to find time to write some code.