FakeRest
Intercept AJAX calls to fake a REST server based on JSON data. Use it on top of Sinon.js (for XMLHTTPRequest
) or fetch-mock (for fetch
) to test JavaScript REST clients on the browser side (e.g. single page apps) without a server.
Usage
Fake XMLHTTPRequest
<script src="/path/to/FakeRest.min.js"></script>
<script src="/path/to/sinon.js"></script>
<script type="text/javascript">
var data = {
'authors': [
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' },
{ id: 1, first_name: 'Jane', last_name: 'Austen' }
],
'books': [
{ id: 0, author_id: 0, title: 'Anna Karenina' },
{ id: 1, author_id: 0, title: 'War and Peace' },
{ id: 2, author_id: 1, title: 'Pride and Prejudice' },
{ id: 3, author_id: 1, title: 'Sense and Sensibility' }
],
'settings': {
language: 'english',
preferred_format: 'hardback',
}
};
// initialize fake REST server
var restServer = new FakeRest.Server();
restServer.init(data);
// use sinon.js to monkey-patch XmlHttpRequest
var server = sinon.fakeServer.create();
server.respondWith(restServer.getHandler());
</script>
Fake fetch
import fetchMock from 'fetch-mock';
import FakeRest from 'fakerest';
var data = {
'authors': [
{ id: 0, first_name: 'Leo', last_name: 'Tolstoi' },
{ id: 1, first_name: 'Jane', last_name: 'Austen' }
],
'books': [
{ id: 0, author_id: 0, title: 'Anna Karenina' },
{ id: 1, author_id: 0, title: 'War and Peace' },
{ id: 2, author_id: 1, title: 'Pride and Prejudice' },
{ id: 3, author_id: 1, title: 'Sense and Sensibility' }
],
'settings': {
language: 'english',
preferred_format: 'hardback',
}
};
const restServer = new FakeRest.FetchServer('http://localhost:3000');
restServer.init(data);
fetchMock.mock('begin:http://localhost:3000', restServer.getHandler());
FakeRest will now intercept every XmlHttpRequest
to the REST server. The handled routes for collections of items are:
GET /:resource
POST /:resource
GET /:resource/:id
PUT /:resource/:id
PATCH /:resource/:id
DELETE /:resource/:id
The handled routes for single items are:
GET /:resource
PUT /:resource
PATCH /:resource
Let's see an example:
// Query the fake REST server
var req = new XMLHttpRequest();
req.open("GET", "/authors", false);
req.send(null);
console.log(req.responseText);
// [
// {"id":0,"first_name":"Leo","last_name":"Tolstoi"},
// {"id":1,"first_name":"Jane","last_name":"Austen"}
// ]
var req = new XMLHttpRequest();
req.open("GET", "/books/3", false);
req.send(null);
console.log(req.responseText);
// {"id":3,"author_id":1,"title":"Sense and Sensibility"}
var req = new XMLHttpRequest();
req.open("GET", "/settings", false);
req.send(null);
console.log(req.responseText);
// {"language:"english","preferred_format":"hardback"}
var req = new XMLHttpRequest();
req.open("POST", "/books", false);
req.send(JSON.stringify({ author_id: 1, title: 'Emma' }));
console.log(req.responseText);
// {"author_id":1,"title":"Emma","id":4}
// restore native XHR constructor
server.restore();
Tip: The fakerServer
provided by Sinon.js is available as a standalone library, without the entire stubbing framework. Simply add the following bower dependency:
devDependencies: {
"sinon-server": "http://sinonjs.org/releases/sinon-server-1.14.1.js"
}
Installation
FakeRest is available through npm and Bower:
# If you use Bower
bower install fakerest --save-dev
# If you use npm
npm install fakerest --save-dev
REST Flavor
FakeRest defines a REST flavor, described below. It is inspired by commonly used ways how to handle aspects like filtering and sorting.
-
GET /foo
returns a JSON array. It accepts three query parameters:filter
,sort
, andrange
. It responds with a status 200 if there is no pagination, or 206 if the list of items is paginated. The response contains a mention of the total count in theContent-Range
header.GET /books?filter={"author_id":1}&embed=["author"]&sort=["title","desc"]&range=[0-9] HTTP 1.1 200 OK Content-Range: items 0-1/2 Content-Type: application/json [ { "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, { "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } ]
The
filter
param must be a serialized object literal describing the criteria to apply to the search query.GET /books?filter={"author_id":1} // return books where author_id is equal to 1 HTTP 1.1 200 OK Content-Range: items 0-1/2 Content-Type: application/json [ { "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, { "id": 3, "author_id": 1, "title": "Sense and Sensibility" } ] // array values are possible GET /books?filter={"id":[2,3]} // return books where id is in [2,3] HTTP 1.1 200 OK Content-Range: items 0-1/2 Content-Type: application/json [ { "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, { "id": 3, "author_id": 1, "title": "Sense and Sensibility" } ] // use the special "q" filter to make a full-text search on all text fields GET /books?filter={"q":"and"} // return books where any of the book properties contains the string 'and' HTTP 1.1 200 OK Content-Range: items 0-2/3 Content-Type: application/json [ { "id": 1, "author_id": 0, "title": "War and Peace" }, { "id": 2, "author_id": 1, "title": "Pride and Prejudice" }, { "id": 3, "author_id": 1, "title": "Sense and Sensibility" } ] // use _gt, _gte, _lte, _lt, or _neq suffix on filter names to make range queries GET /books?filter={"price_lte":20} // return books where price is less than or equal to 20 GET /books?filter={"price_gt":20} // return books where price is greater than 20 // when the filter object contains more than one property, the criteria combine with an AND logic GET /books?filter={"published_at_gte":"2015-06-12","published_at_lte":"2015-06-15"} // return books published between two dates
The
embed
param sets the related objects or collections to be embedded in the response.// embed author in books GET /books?embed=["author"] HTTP 1.1 200 OK Content-Range: items 0-3/4 Content-Type: application/json [ { "id": 0, "author_id": 0, "title": "Anna Karenina", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, { "id": 1, "author_id": 0, "title": "War and Peace", "author": { "id": 0, "first_name": "Leo", "last_name": "Tolstoi" } }, { "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }, { "id": 3, "author_id": 1, "title": "Sense and Sensibility", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } } ] // embed books in author GET /authors?embed=["books"] HTTP 1.1 200 OK Content-Range: items 0-1/2 Content-Type: application/json [ { id: 0, first_name: 'Leo', last_name: 'Tolstoi', books: [{ id: 0, author_id: 0, title: 'Anna Karenina' }, { id: 1, author_id: 0, title: 'War and Peace' }] }, { id: 1, first_name: 'Jane', last_name: 'Austen', books: [{ id: 2, author_id: 1, title: 'Pride and Prejudice' }, { id: 3, author_id: 1, title: 'Sense and Sensibility' }] } ] // you can embed several objects GET /authors?embed=["books","country"]
The
sort
param must be a serialized array literal defining first the property used for sorting, then the sorting direction.GET /author?sort=["date_of_birth","asc"] // return authors, the oldest first GET /author?sort=["date_of_birth","desc"] // return authors, the youngest first
The
range
param defines the number of results by specifying the rank of the first and last result. The first result is #0.GET /books?range=[0-9] // return the first 10 books GET /books?range=[10-19] // return the 10 next books
-
POST /foo
returns a status 201 with aLocation
header for the newly created resource, and the new resource in the body.POST /books { "author_id": 1, "title": "Emma" } HTTP 1.1 201 Created Location: /books/4 Content-Type: application/json { "author_id": 1, "title": "Emma", "id": 4 }
-
GET /foo/:id
returns a JSON object, and a status 200, unless the resource doesn't existGET /books/2 HTTP 1.1 200 OK Content-Type: application/json { "id": 2, "author_id": 1, "title": "Pride and Prejudice" }
The
embed
param sets the related objects or collections to be embedded in the response.GET /books/2?embed=['author'] HTTP 1.1 200 OK Content-Type: application/json { "id": 2, "author_id": 1, "title": "Pride and Prejudice", "author": { "id": 1, "first_name": "Jane", "last_name": "Austen" } }
-
PUT /foo/:id
returns the modified JSON object, and a status 200, unless the resource doesn't exist -
DELETE /foo/:id
returns the deleted JSON object, and a status 200, unless the resource doesn't exist
If the REST flavor you want to simulate differs from the one chosen for FakeRest, no problem: request and response interceptors will do the conversion (see below).
Note that all of the above apply only to collections. Single objects respond to GET /bar
, PUT /bar
and PATCH /bar
in a manner identical to those operations for /foo/:id
, including embedding. POST /bar
and DELETE /bar
are not enabled.
Supported Filters
Operators are specified as suffixes on each filtered field. For instance, applying the _lte
operator on the price
field for the books
resource is done by like this:
GET /books?filter={"price_lte":20} // return books where price is less than or equal to 20
-
_eq
: check for equality on simple values:GET /books?filter={"price_eq":20} // return books where price is equal to 20
-
_neq
: check for inequality on simple valuesGET /books?filter={"price_neq":20} // return books where price is not equal to 20
-
_eq_any
: check for equality on any passed valuesGET /books?filter={"price_eq_any":[20, 30]} // return books where price is equal to 20 or 30
-
_neq_any
: check for inequality on any passed valuesGET /books?filter={"price_neq_any":[20, 30]} // return books where price is not equal to 20 nor 30
-
_inc_any
: check for items that includes any of the passed valuesGET /books?filter={"authors_inc_any":['William Gibson', 'Pat Cadigan']} // return books where authors includes either 'William Gibson' or 'Pat Cadigan' or both
-
_q
: check for items that contains the provided textGET /books?filter={"author_q":['Gibson']} // return books where author includes 'Gibson' not considering the other fields
-
_lt
: check for items that has a value lower than the provided valueGET /books?filter={"price_lte":100} // return books that have a price lower that 100
-
_lte
: check for items that has a value lower or equal than the provided valueGET /books?filter={"price_lte":100} // return books that have a price lower or equal to 100
-
_gt
: check for items that has a value greater than the provided valueGET /books?filter={"price_gte":100} // return books that have a price greater that 100
-
_gte
: check for items that has a value greater or equal than the provided valueGET /books?filter={"price_gte":100} // return books that have a price greater or equal to 100
Usage and Configuration
// initialize a rest server with a custom base URL
var restServer = new FakeRest.Server('http://my.custom.domain'); // // only URLs starting with my.custom.domain will be intercepted
restServer.toggleLogging(); // logging is off by default, enable it to see network calls in the console
// Set all JSON data at once - only if identifier name is 'id'
restServer.init(json);
// modify the request before FakeRest handles it, using a request interceptor
// request is {
// url: '...',
// headers: [...],
// requestBody: '...',
// json: ..., // parsed JSON body
// queryString: '...',
// params: {...} // parsed query string
// }
restServer.addRequestInterceptor(function(request) {
var start = (request.params._start - 1) || 0;
var end = request.params._end !== undefined ? (request.params._end - 1) : 19;
request.params.range = [start, end];
return request; // always return the modified input
});
// modify the response before FakeRest sends it, using a response interceptor
// response is {
// status: ...,
// headers: [...],
// body: {...}
// }
restServer.addResponseInterceptor(function(response) {
response.body = { data: response.body, status: response.status };
return response; // always return the modified input
});
// set default query, e.g. to force embeds or filters
restServer.setDefaultQuery(function(resourceName) {
if (resourceName == 'authors') return { embed: ['books'] }
if (resourceName == 'books') return { filter: { published: true } }
return {};
})
// enable batch request handler, i.e. allow API clients to query several resourecs into a single request
// see [Facebook's Batch Requests philosophy](https://developers.facebook.com/docs/graph-api/making-multiple-requests) for more details.
restServer.setBatchUrl('/batch');
// you can create more than one fake server to listen to several domains
var restServer2 = new FakeRest.Server('http://my.other.domain');
// Set data collection by collection - allows to customize the identifier name
var authorsCollection = new FakeRest.Collection([], '_id');
authorsCollection.addOne({ first_name: 'Leo', last_name: 'Tolstoi' }); // { _id: 0, first_name: 'Leo', last_name: 'Tolstoi' }
authorsCollection.addOne({ first_name: 'Jane', last_name: 'Austen' }); // { _id: 1, first_name: 'Jane', last_name: 'Austen' }
// collections have autoincremented identifier but accept identifiers already set
authorsCollection.addOne({ _id: 3, first_name: 'Marcel', last_name: 'Proust' }); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' }
restServer2.addCollection('authors', authorsCollection);
// collections are mutable
authorsCollection.updateOne(1, { last_name: 'Doe' }); // { _id: 1, first_name: 'Jane', last_name: 'Doe' }
authorsCollection.removeOne(3); // { _id: 3, first_name: 'Marcel', last_name: 'Proust' }
var server = sinon.fakeServer.create();
server.autoRespond = true;
server.respondWith(restServer.getHandler());
server.respondWith(restServer2.getHandler());
Development
# Install dependencies
make install
# Watch source files and recompile dist/FakeRest.js when anything is modified
make watch
# Run tests
make test
# Build minified version
make build
License
FakeRest is licensed under the MIT Licence, sponsored by marmelab.