marmelab / restful.js

A pure JS client for interacting with server-side RESTful resources. Think Restangular without Angular.
MIT License
966 stars 81 forks source link
frontend http-client rest
archived Archived Repository
This code is no longer maintained. Feel free to fork it, but use it at your own risks.

restful.js Build Status

A pure JS client for interacting with server-side RESTful resources. Think Restangular without Angular.

Note: All examples written in this README use the ES6 specification.

Installation

It is available with bower or npm:

bower install restful.js
npm install restful.js

The dist folder contains two built versions which you can use to include either restful.js or a standalone version. Standalone version already embeds fetch.

Alternately, you can use a module loader like webpack.

import restful from 'restful.js';

Usage

Create a resource targeting your API

Restful.js needs an HTTP backend in order to perform queries. Two http backend are currently available:

Start by defining the base endpoint for an API, for instance http://api.example.com with the good http backend.

For a browser build :

import whatwg-fetch;
import restful, { fetchBackend } from 'restful.js';

const api = restful('http://api.example.com', fetchBackend(fetch));

For a node build :

import request from 'request';
import restful, { requestBackend } from 'restful.js';

const api = restful('http://api.example.com', requestBackend(request));

For those who prefers a ready-to-go version, pre built version of restful.js with fetch are available into the dist folder.

Collections and Members endpoints

A collection is an API endpoint for a list of entities, for instance http://api.example.com/articles. Create it using the all(name) syntax:

const articlesCollection = api.all('articles');  // http://api.example.com/articles

articlesCollection is just the description of the collection, the API wasn't fetched yet.

A member is an API endpoint for a single entity, for instance http://api.example.com/articles/1. Create it using the one(name, id) syntax:

const articleMember = api.one('articles', 1);  // http://api.example.com/articles/1

Just like above, articleMember is a description, not an entity.

You can chain one() and all() to target the required collection or member:

const articleMember = api.one('articles', 1);  // http://api.example.com/articles/1
const commentsCollection = articleMember.all('comments');  // http://api.example.com/articles/1/comments

Custom endpoint URL

In case you need to set a custom endpoint URL, you can use custom methods.

const articleCustom = api.custom('articles/beta');  // http://api.example.com/articles/beta

// you can add an absolute url
const articleCustom = api.custom('http://custom.url/articles/beta', false);  // http://custom.url/articles/beta

A custom endpoint acts like a member, and therefore you can use one and all to chain other endpoint with it.

Entities

Once you have collections and members endpoints, fetch them to get entities. Restful.js exposes get() and getAll() methods for fetching endpoints. Since these methods are asynchronous, they return a native Promise for response.

If your application does not support native Promise, you can use a polyfill.

const articleMember = api.one('articles', 1);  // http://api.example.com/articles/1
articleMember.get().then((response) => {
    const articleEntity = response.body();

    const article = articleEntity.data();
    console.log(article.title); // hello, world!
});

const commentsCollection = articleMember.all('comments');  // http://api.example.com/articles/1/comments
commentsCollection.getAll().then((response) => {
    const commentEntities = response.body();

    commentEntities.forEach((commentEntity) => {
        const comment = commentEntity.data();
        console.log(comment.body);
    })
});

Tip: You can describe a member based on a collection and trigger the API fetch at the same time by calling get(id):

// fetch http://api.example.com/articles/1/comments/4
const articleMember = api.one('articles', 1);
const commentMember = articleMember.one('comments', 4);
commentMember.get().then((response) => {
    //
});
// equivalent to
const commentsCollection = articleMember.all('comments');
commentsCollection.get(4).then((response) => {
    //
});

Response

A response is made from the HTTP response fetched from the endpoint. It exposes statusCode(), headers(), and body() methods. For a GET request, the body method will return one or an array of entities. Therefore you can disable this hydration by calling body(false).

Headers

For most of cases, headers in a response will be a plain object with headers data, but for some browsers, that don't support iteration over Headers object, it will simply be returned as a Headers object, so you can use get method from it to get required headers.

Entity Data

An entity is made from the HTTP response data fetched from the endpoint. It exposes a data() method:

const articleCollection = api.all('articles');  // http://api.example.com/articles

// http://api.example.com/articles/1
api.one('articles', 1).get().then((response) => {
    const articleEntity = response.body();

    // if the server response was { id: 1, title: 'test', body: 'hello' }
    const article = articleEntity.data();
    article.title; // returns `test`
    article.body; // returns `hello`
    // You can also edit it
    article.title = 'test2';
    // Finally you can easily update it or delete it
    articleEntity.save(); // will perform a PUT request
    articleEntity.delete(); // will perform a DELETE request
}, (response) => {
    // The reponse code is not >= 200 and < 400
    throw new Error('Invalid response');
});

You can also use the entity to continue exploring the API. Entities expose several other methods to chain calls:

const articleMember = api.one('articles', 1);  // http://api.example.com/articles/1
const commentMember = articleMember.one('comments', 3);  // http://api.example.com/articles/1/comments/3
commentMember.get()
    .then((response) => {
        const commentEntity = response.body();

        // You can also call `all` and `one` on an entity
        return comment.all('authors').getAll(); // http://api.example.com/articles/1/comments/3/authors
    }).then((response) => {
        const authorEntities = response.body();

        authorEntities.forEach((authorEntity) => {
            const author = authorEntity.data();
            console.log(author.name);
        });
    });

entity.id() will get the id from its data regarding of the identifier of its endpoint. If you are using another name than id you can modify it by calling identifier() on the endpoint.

const articleCollection = api.all('articles');  // http://api.example.com/articles
articleCollection.identifier('_id'); // We use _id as id field

const articleMember = api.one('articles', 1);  // http://api.example.com/articles/1
articleMember.identifier('_id'); // We use _id as id field

Restful.js uses an inheritance pattern when collections or members are chained. That means that when you configure a collection or a member, it will configure all the collection on members chained afterwards.

// configure the api
api.header('AuthToken', 'test');
api.identifier('_id');

const articlesCollection = api.all('articles');
articlesCollection.get(1); // will send the `AuthToken` header
// You can configure articlesCollection, too
articlesCollection.header('foo', 'bar');
articlesCollection.one('comments', 1).get(); // will send both the AuthToken and foo headers

API Reference

Restful.js exposes similar methods on collections, members and entities. The name are consistent, and the arguments depend on the context.

Collection methods

// http://api.example.com/articles/1/comments/2/authors
const authorsCollection = api.one('articles', 1).one('comments', 2).all('authors');
authorsCollection.getAll().then((authorEntities) => { /*  */ });
authorsCollection.get(1).then((authorEntity) => { /*  */ });

Member methods

// http://api.example.com/articles/1/comments/2
const commentMember = api.one('articles', 1).one('comments', 2);
commentMember.get().then((commentEntity) => { /*  */ });
commentMember.delete().then((data) => { /* */ });

Interceptors

An error, response or request interceptor is a callback which looks like this:

resource.addRequestInterceptor((config) => {
    const { data, headers, method, params, url } = config;
    // all args had been modified
    return {
        data,
        params,
        headers,
        method,
        url,
    };

    // just return modified arguments
    return {
        data,
        headers,
    };
});

resource.addResponseInterceptor((response, config) => {
    const { data, headers, statusCode } = response;
    // all args had been modified
    return {
        data,
        headers,
        statusCode
    };

    // just return modified arguments
    return {
        data,
        headers,
    };
});

resource.addErrorInterceptor((error, config) => {
    const { message, response } = error;
    // all args had been modified
    return {
        message,
        response,
    };

    // just return modified arguments
    return {
        message,
    };
});

Response methods

Entity methods

// http://api.example.com/articles/1/comments/2
const commentMember = api.one('articles', 1).one('comments', 2);
commentMember.get().then((commentEntity) => {
    commentEntity.save();
    commentEntity.remove();
});

Error Handling

To deal with errors, you can either use error interceptors, error callbacks on promise or error events.

const commentMember = resource.one('articles', 1).one('comments', 2);
commentMember
    .get()
    .then((commentEntity) => { /*  */ })
    .catch((err) => {
        // deal with the error
    });

commentMember.on('error', (error, config) => {
    // deal with the error
});

Events

Any endpoint (collection or member) is an event emitter. It emits request, response and error events. When it emits an event, it is propagated to all its parents. This way you can listen to all errors, requests and response on your restful instance by listening on your root endpoint.

api.on('error', (error, config) => {
    // deal with the error
});

api.on('request', (config) => {
    // deal with the request
});

api.on('response', (config) => {
    // deal with the response
});

When you use interceptors, endpoints will also emit request:interceptor:pre, request:interceptor:post, response:interceptor:pre, response:interceptor:post, error:interceptor:pre and error:interceptor:post:

api.on('error:interceptor:pre', (error, config, interceptorName) => {
    // deal with the error
});

api.on('error:interceptor:post', (error, config, interceptorName) => {
    // deal with the error
});

api.on('request:interceptor:pre', (config, interceptorName) => {
    // deal with the request
});

api.on('request:interceptor:post', (config, interceptorName) => {
    // deal with the request
});

api.on('response:interceptor:pre', (response, config, interceptorName) => {
    // deal with the response
});

api.on('response:interceptor:post', (response, config, interceptorName) => {
    // deal with the response
});

You can also use once method to add a one shot event listener.

Development

Install dependencies:

make install

Development Build

To rebuild the JavaScript you must run: make build-dev.

During development you can run make watch to trigger a build at each change.

Production build

To build for production (minified files) you must run: make build.

ES5 build

To build the ES5 files you must run: make es5.

Tests

make test

Contributing

All contributions are welcome and must pass the tests. If you add a new feature, please write tests for it.

License

This application is available under the MIT License, courtesy of marmelab.