First commit

This commit is contained in:
Typicode
2013-11-30 14:42:14 +01:00
commit b8915e7bb8
18 changed files with 1091 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 typicode
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

179
README.md Normal file
View File

@ -0,0 +1,179 @@
![](http://i.imgur.com/dLeJmw6.png)
__JSON Server__
> Give it a JSON or JS seed file and it will serve it through REST routes.
_Created with :heart: for front-end developers who need a flexible back-end for quick prototyping and mocking._
## Examples
### Command line interface
```bash
$ cat db.json
{
posts: [
{ id: 1, body: 'foo' }
]
}
$ json-server --file db.json
$ curl -i http://localhost:3000/posts/1
```
### Node module
```javascript
var server = require('json-server');
var db = {
posts: [
{id: 1, body: 'foo'}
]
}
server.run(db);
```
## Why?
* Supports __GET__ but also __POST__, __PUT__, __DELETE__ and even __PATCH__ requests
* Can be used from anywhere through __cross domain__ requests (JSONP or CORS)
* Can load remote JSON files
* Can be deployed on Nodejitsu, Heroku, ...
## Installation
```bash
$ npm install -g json-server
```
## Usage
### Command line interface
```bash
json-server --help
Usage: json-server [options]
Options:
-h, --help output usage information
-V, --version output the version number
-f --file <file> load db from a js or json file
-u --url <url> load db from a URL
-p --port [port] server port
--read-only read only mode
```
JSON Server can load JSON from multiple sources:
```bash
$ json-server --file db.json
$ json-server --file seed.js
$ json-server --url http://example.com/db.json
```
And be run in read-only mode (useful if deployed on a public server):
```bash
$ json-server --file db.json --read-only
```
#### Input
Here's 2 examples showing how to format JSON or JS seed file:
* __db.json__
```javascript
{
posts: [
{ id: 1, body: 'foo'},
{ id: 2, body: 'bar'}
],
comments: [
{ id: 1, body: 'baz', postId: 1}
{ id: 2, body: 'qux', postId: 2}
]
}
```
* __seed.js__
```javascript
exports.run = function() {
var data = {};
data.posts = [];
data.posts.push({id: 1, body: 'foo'});
//...
return data;
}
```
JSON Server expects JS files to export a ```run``` method that returns an object.
Seed files are useful if you need to programmaticaly create a lot of data.
### Node module
#### run(db, [options])
```javascript
var server = require('json-server'),
db = require('./seed').run();
var options = { port: 4000, readOnly: true };
server.run(db, options);
```
By default, ```port``` is set to 3000 and ```readOnly``` to false.
## Routes
```
GET /:resource
GET /:resource?attr=&attr=&
GET /:parent/:parentId/:resource
GET /:resource/:id
POST /:resource
PUT /:resource/:id
PATCH /:resource/:id
DEL /:resource/:id
```
For routes usage information, have a look at [JSONPlaceholder](https://github.com/typicode/jsonplaceholder) code examples.
```
GET /db
```
Returns database state.
```
GET /
```
Returns default index file or content of ./public/index.html (useful if you need to set a custom home page).
## Support
If you like the project, please tell your friends about it, star it or give feedback :) It's very much appreciated!
For project updates or to get in touch, [@typicode](http://twitter.com/typicde). You can also send me a mail.
## Test
```bash
$ npm install
$ npm test
```

48
bin/cli.js Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env node
var program = require('commander'),
request = require('superagent'),
url = require('url'),
fs = require('fs'),
server = require('../server'),
logger = require('../utils/logger'),
options = {};
function loadFile(file, cb) {
var path = process.cwd() + '/' + file,
db;
if (/\.json$/.test(file)) db = require(path);
if (/\.js$/.test(file)) db = require(path).run();
cb(db);
}
function loadURL(url, cb) {
logger.info('Fetching ' + url + '...')
request
.get(url)
.end(function(error, res) {
if (error) {
logger.error(error);
} else {
cb(JSON.parse(res));
}
});
}
function onDatabaseLoaded(db) {
server.run(db, options);
}
program
.version('0.1.0')
.option('-f --file <file>', 'load db from a js or json file')
.option('-u --url <url>', 'load db from a URL')
.option('-p --port [port]', 'server port')
.option('--read-only', 'read only mode')
.parse(process.argv);
if (program.port) options.port = program.port;
if (program.readOnly) options.readOnly = true;
if (program.file) loadFile(program.file, onDatabaseLoaded);
if (program.url) loadURL(program.url, onDatabaseLoaded);

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "json-server",
"version": "0.1.0",
"description": "Serves JSON file through REST routes",
"main": "server.js",
"bin": "./bin/cli.js",
"directories": {
"test": "test"
},
"dependencies": {
"commander": "~2.0.0",
"cors": "~2.1.0",
"express": "~3.4.4",
"logan": "~0.0.2",
"request": "~2.27.0",
"underscore": "~1.5.2",
"underscore.inflections": "~0.2.1"
},
"devDependencies": {
"superagent": "~0.15.7",
"supertest": "~0.8.1"
},
"scripts": {
"test": "mocha -R spec test",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git://github.com/typicode/json-server.git"
},
"keywords": [
"JSON",
"server",
"fake",
"REST",
"API",
"prototyping",
"mock",
"mocking",
"test",
"testing",
"rest",
"data",
"dummy"
],
"author": "Typicode <typicode@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/typicode/json-server/issues"
},
"homepage": "https://github.com/typicode/json-server"
}

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

79
public/index.html Normal file
View File

@ -0,0 +1,79 @@
<html>
<head>
<title>JSON Server</title>
<link href="//netdna.bootstrapcdn.com/bootswatch/3.0.2/journal/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="stylesheets/style.css">
</head>
<body>
<div class="container">
<a href="https://github.com/typicode/json-server" class="logo">
<img src="images/logo.png">
</a>
<p class="alert alert-info">
Congrats! You're successfully running JSON Server.
</p>
<hr>
<h2>Resources</h2>
<p>
<ul id="resources">loading, please wait...</ul>
</p>
<p>
To view database current state:
<ul>
<li>
<a href="db">db</a>
</li>
</ul>
</p>
<h2>Requests</h2>
<p>
Resources can be accessed in various ways.
</p>
<p>
JSON Server supports:
<ul>
<li>GET, POST, PUT, PATCH, DESTROY and OPTIONS verbs.</li>
<li>JSONP or CORS cross domain requests.</li>
</ul>
</p>
<h2>Documentation</h2>
<p>
View
<a href="http://github.com/typicode/jsonserver">README</a>
on GitHub.
</p>
<h2>Issues</h2>
<p>Please go
<a href="https://github.com/typicode/jsonplaceholder/issues">here</a>.
</p>
<hr>
<p>
<i>To customize this page, just create a ./public/index.html file.</i>
</p>
</div>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script>
$(function() {
$.get('db').then(function(data) {
$('#resources').empty();
$.each(data, function(key, value) {
$('#resources')
.append('<li><a href="/'+ key + '">' + key + '</a></li>');
})
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,43 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
h1 {
font-weight: lighter;
}
a {
/*color: #00B7FF;*/
color: #3D72D1;
}
hr {
border: 1px #EEE solid;
}
a.logo {
display: block;
text-align: center;
margin-bottom: 50px;
}
.alert-success {
font-size: 18px;
}
/*.logo {
background: #00B7FF;
color: white;
font-size: 48px;
width: 100px;
height: 100px;
text-align: center;
vertical-align: middle;
line-height: 90px;
border-radius: 100px;
transform:rotate(90deg);
-ms-transform:rotate(90deg);
-webkit-transform:rotate(90deg);
margin-bottom: 50px;
}*/

71
routes/read-only.js Normal file
View File

@ -0,0 +1,71 @@
var _ = require('underscore'),
utils = require('../utils/utils'),
db = {};
_.mixin(require('../utils/db-mixins'));
exports.setDatabase = function(object) {
db = object;
}
exports.database = function(req, res) {
res.jsonp(db)
}
// GET /:resource?attr=&attr=
exports.list = function(req, res) {
var collection = db[req.params.resource],
properties = {},
result;
Object.keys(req.query).forEach(function (key) {
var value = req.query[key];
properties[key] = utils.toNative(value);
});
if (_(properties).isEmpty()) {
result = collection;
} else {
result = _(collection).where(properties);
}
res.jsonp(result);
}
// GET /:parent/:parentId/:resource
exports.nestedList = function(req, res) {
var properties = {},
resource;
// Set parentID
properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId;
// Filter using parentID
resource = _.where(db[req.params.resource], properties);
res.jsonp(resource);
}
// GET /:resource/:id
exports.show = function(req, res) {
var resource = _.get(db, req.params.resource, +req.params.id);
res.jsonp(resource);
}
exports.create = function(req, res) {
req.body.id = Math.round(new Date().getTime() / 1000);
res.jsonp(req.body);
}
exports.update = function(req, res) {
var resource = _.get(db, req.params.resource, +req.params.id),
clonedResource = _.clone(resource),
result = _.extend(clonedResource, req.body);
res.jsonp(result);
}
exports.destroy = function(req, res) {
res.send(204)
}

79
routes/read-write.js Normal file
View File

@ -0,0 +1,79 @@
var _ = require('underscore'),
utils = require('../utils/utils'),
db = {};
_.mixin(require('../utils/db-mixins'));
exports.setDatabase = function(object) {
db = object;
}
exports.database = function(req, res) {
res.jsonp(db)
}
// GET /:resource?attr=&attr=
exports.list = function(req, res) {
var collection = db[req.params.resource],
properties = {},
result;
Object.keys(req.query).forEach(function (key) {
var value = req.query[key];
properties[key] = utils.toNative(value);
});
if (_(properties).isEmpty()) {
result = collection;
} else {
result = _(collection).where(properties);
}
res.jsonp(result);
}
// GET /:parent/:parentId/:resource
exports.nestedList = function(req, res) {
var properties = {},
resource;
// Set parentID
properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId;
// Filter using parentID
resource = _.where(db[req.params.resource], properties);
res.jsonp(resource);
}
// GET /:resource/:id
exports.show = function(req, res) {
var resource = _.get(db, req.params.resource, +req.params.id);
res.jsonp(resource);
}
// POST /:resource
exports.create = function(req, res) {
var resource = _.create(db, req.params.resource, req.body);
res.jsonp(resource);
}
// PUT /:resource/:id
// PATCH /:resource/:id
exports.update = function(req, res) {
_.update(db, req.params.resource, +req.params.id, req.body);
var resource = _.get(db, req.params.resource, +req.params.id);
res.jsonp(resource);
}
// DELETE /:resource/:id
exports.destroy = function(req, res) {
_.remove(db, req.params.resource, +req.params.id);
_.clean(db);
res.send(204);
}

95
server.js Normal file
View File

@ -0,0 +1,95 @@
var express = require('express'),
cors = require('cors'),
http = require('http'),
path = require('path'),
fs = require('fs'),
_ = require('underscore'),
logger = require('./utils/logger');
var defaultOptions = {
port: process.env.PORT || 3000,
readOnly: false
}
function createApp(db, options) {
// Create app
var app = express(),
options = options || {},
routes;
// Configure all environments
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
// Configure development
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
// Configure using options provided
app.set('port', options.port);
routes = options.readOnly ? './routes/read-only' : './routes/read-write';
routes = require(routes);
// Use default or user public directory
// Note: should be done before CORS and app.router setting
console.log(fs.existsSync(process.cwd() + '/public'), process.cwd() + '/public')
if (fs.existsSync(process.cwd() + '/public')) {
app.use(express.static(process.cwd() + '/public'));
} else {
app.use(express.static(path.join(__dirname, './public')));
}
// Enable CORS for everything
app.use(cors());
app.options('*', cors());
// Set app.router
app.use(app.router);
// Set API entry points
app.get('/db', routes.database)
app.get('/:resource', routes.list);
app.get('/:parent/:parentId/:resource', routes.nestedList);
app.get('/:resource/:id', routes.show);
app.post('/:resource', routes.create);
app.put('/:resource/:id', routes.update);
app.patch('/:resource/:id', routes.update);
app.del('/:resource/:id', routes.destroy);
// Set database
routes.setDatabase(db);
// And done! Ready to serve JSON!
return app;
}
function run(db, options) {
options = _.defaults(options, defaultOptions);
var app = createApp(db, options);
if (_.isEmpty(db)) {
logger.error('No resources found!');
} else {
logger.success('Available resources');
for (var prop in db) {
logger.url(options.port, prop);
}
}
http
.createServer(app)
.listen((options.port), function(){
logger.success('Express server listening on port ' + options.port);
logger.success('Congrats! Open http://localhost:' + options.port);
});
}
exports.createApp = createApp;
exports.run = run;

18
test/fixture.js Normal file
View File

@ -0,0 +1,18 @@
// Small database to be used during tests
module.exports = function() {
var db = {};
db.posts = [
{id: 1, body: 'foo'},
{id: 2, body: 'bar'}
]
db.comments = [
{id: 1, published: true, postId: 1},
{id: 2, published: false, postId: 1},
{id: 3, published: false, postId: 2},
{id: 4, published: false, postId: 2},
]
return db;
}

138
test/read-only.js Normal file
View File

@ -0,0 +1,138 @@
var request = require('supertest'),
assert = require('assert'),
server = require('../server'),
fixture = require('./fixture'),
db,
app;
describe('Read only routes', function() {
beforeEach(function() {
db = fixture();
app = server.createApp(db, { readOnly: true });
});
describe('GET /:resource', function() {
it('should respond with json and resources and corresponding resources', function(done) {
request(app)
.get('/posts')
.expect('Content-Type', /json/)
.expect(db.posts)
.expect(200, done);
});
});
describe('GET /:resource?attr=&attr=', function() {
it('should respond with json and filter resources', function(done) {
request(app)
.get('/comments?postId=1&published=true')
.expect('Content-Type', /json/)
.expect([db.comments[0]])
.expect(200, done);
});
});
describe('GET /:parent/:parentId/:resource', function() {
it('should respond with json and corresponding nested resources', function(done) {
request(app)
.get('/posts/1/comments')
.expect('Content-Type', /json/)
.expect([db.comments[0], db.comments[1]])
.expect(200, done);
});
});
describe('GET /:resource/:id', function() {
it('should respond with json and corresponding resource', function(done) {
request(app)
.get('/posts/1')
.expect('Content-Type', /json/)
.expect(db.posts[0])
.expect(200, done);
});
});
describe('GET /db', function() {
it('should respond with json and full database', function(done) {
request(app)
.get('/db')
.expect('Content-Type', /json/)
.expect(db)
.expect(200, done);
});
});
describe('POST /:resource', function() {
it('should respond with fake json and not create a resource', function(done) {
request(app)
.post('/posts')
.send({body: '...'})
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res){
if (err) return done(err);
assert(res.body.hasOwnProperty('id'));
assert.equal(res.body.body, '...');
assert.equal(db.posts.length, 2);
done()
});
});
});
describe('PUT /:resource/:id', function() {
it('should respond with fake json and not update resource', function(done) {
request(app)
.put('/posts/1')
.send({id: 999, body: '...'})
.expect('Content-Type', /json/)
.expect({id: 999, body: '...'})
.expect(200)
.end(function(err, res){
if (err) return done(err);
// Checking that first post wasn't updated
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
done()
});
});
});
describe('PATCH /:resource/:id', function() {
it('should respond with fake json and not update resource', function(done) {
request(app)
.patch('/posts/1')
.send({body: '...'})
.expect('Content-Type', /json/)
.expect({id: 1, body: '...'})
.expect(200)
.end(function(err, res){
if (err) return done(err);
// Checking that first post wasn't updated
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
done()
});
});
});
describe('DELETE /:resource/:id', function() {
it('should respond with empty data and not destroy resource', function(done) {
request(app)
.del('/posts/1')
.expect(204)
.end(function(err, res){
if (err) return done(err);
assert.equal(db.posts.length, 2);
assert.equal(db.comments.length, 4);
done()
});
});
});
describe('OPTIONS /:resource/:id', function() {
it('should respond with empty data and not destroy resource', function(done) {
request(app)
.options('/posts/1')
.expect(204, done);
});
});
});

130
test/read-write.js Normal file
View File

@ -0,0 +1,130 @@
var request = require('supertest'),
assert = require('assert'),
server = require('../server'),
routes = require('../routes/read-write'),
fixture = require('./fixture'),
db,
app;
describe('Read write routes', function() {
beforeEach(function() {
db = fixture();
app = server.createApp(db);
});
describe('GET /:resource', function() {
it('should respond with json and corresponding resources', function(done) {
request(app)
.get('/posts')
.expect('Content-Type', /json/)
.expect(db.posts)
.expect(200, done);
});
});
describe('GET /:resource?attr=&attr=', function() {
it('should respond with json and filter resources', function(done) {
request(app)
.get('/comments?postId=1&published=true')
.expect('Content-Type', /json/)
.expect([db.comments[0]])
.expect(200, done);
});
});
describe('GET /:parent/:parentId/:resource', function() {
it('should respond with json and corresponding nested resources', function(done) {
request(app)
.get('/posts/1/comments')
.expect('Content-Type', /json/)
.expect([
db.comments[0],
db.comments[1]
])
.expect(200, done);
});
});
describe('GET /:resource/:id', function() {
it('should respond with json and corresponding resource', function(done) {
request(app)
.get('/posts/1')
.expect('Content-Type', /json/)
.expect(db.posts[0])
.expect(200, done);
});
});
describe('GET /db', function() {
it('should respond with json and full database', function(done) {
request(app)
.get('/db')
.expect('Content-Type', /json/)
.expect(db)
.expect(200, done);
});
});
describe('POST /:resource', function() {
it('should respond with json and create a resource', function(done) {
request(app)
.post('/posts')
.send({body: 'foo'})
.expect('Content-Type', /json/)
.expect({id: 3, body: 'foo'})
.expect(200)
.end(function(err, res){
if (err) return done(err);
assert.equal(db.posts.length, 3);
done();
});
});
});
describe('PUT /:resource/:id', function() {
it('should respond with json and update resource', function(done) {
request(app)
.put('/posts/1')
.send({id: 1, body: 'foo'})
.expect('Content-Type', /json/)
.expect({id: 1, body: 'foo'})
.expect(200)
.end(function(err, res){
if (err) return done(err);
assert.deepEqual(db.posts[0], {id: 1, body: 'foo'});
done();
});
});
});
describe('PATCH /:resource/:id', function() {
it('should respond with json and update resource', function(done) {
request(app)
.patch('/posts/1')
.send({body: 'bar'})
.expect('Content-Type', /json/)
.expect({id: 1, body: 'bar'})
.expect(200)
.end(function(err, res){
if (err) return done(err);
assert.deepEqual(db.posts[0], {id: 1, body: 'bar'});
done();
});
});
});
describe('DELETE /:resource/:id', function() {
it('should respond with empty data, destroy resource and dependent resources', function(done) {
request(app)
.del('/posts/1')
.expect(204)
.end(function(err, res){
if (err) return done(err);
assert.equal(db.posts.length, 1);
assert.equal(db.comments.length, 2);
done();
});
});
});
});

31
test/static.js Normal file
View File

@ -0,0 +1,31 @@
var request = require('supertest'),
assert = require('assert'),
server = require('../server'),
routes = require('../routes/read-write'),
app;
describe('Static routes', function() {
beforeEach(function() {
app = server.createApp({}, routes);
});
describe('GET /', function() {
it('should respond with html', function(done) {
request(app)
.get('/')
.expect('Content-Type', /html/)
.expect(200, done);
});
});
describe('GET /stylesheets/style.css', function() {
it('should respond with css', function(done) {
request(app)
.get('/stylesheets/style.css')
.expect('Content-Type', /css/)
.expect(200, done);
});
});
});

86
utils/db-mixins.js Normal file
View File

@ -0,0 +1,86 @@
(function(root) {
var _ = root._ || require('underscore');
if (!root._) {
_.mixin(require('underscore.inflections'));
}
function get(db, table, id) {
return _.find(db[table], function (row) {
return row.id === id
});
}
function exist(db, table, id) {
return !_.isUndefined(_.get(db, table, id));
}
function createId(db, table) {
if (_.isEmpty(db[table])) {
return 1;
} else {
return _.max(db[table], function(row) {
return row.id;
}).id + 1;
}
}
function create(db, table, obj) {
var clone = _.clone(obj);
if (_.isUndefined(clone.id)) clone.id = _.createId(db, table);
db[table].push(clone);
return clone;
}
function update(db, table, id, attrs) {
var row = get(db, table, id),
updatedRow = _.extend(row, attrs),
index = _.indexOf(db[table], row);
db[table][index] = updatedRow;
}
function clean(db) {
var toBeRemoved = [];
_(db).each(function(table, tableName) {
_(table).each(function(row) {
_(row).each(function(value, key) {
if (/Id$/.test(key)) {
var reference = _.pluralize(key.slice(0, - 2));
if (!_.exist(db, reference, row[key])) {
toBeRemoved.push({
tableName: tableName,
id: row.id
});
}
}
});
});
});
_(toBeRemoved).each(function(row) {
_.remove(db, row.tableName, row.id);
});
}
function remove(db, table, id) {
var newTable = _.reject(db[table], function(row) {
return row.id === id;
});
db[table] = newTable;
}
_.get = get;
_.exist = exist;
_.createId = createId;
_.create = create;
_.update = update;
_.clean = clean;
_.remove = remove;
})(this);

10
utils/logger.js Normal file
View File

@ -0,0 +1,10 @@
var logan = require('logan');
logan.set({
error: ['%', 'red'],
success: ['%', 'green'],
info: ['%', 'grey'],
url: [' http://localhost:%/'.grey + '%'.cyan, '.']
})
module.exports = logan

11
utils/utils.js Normal file
View File

@ -0,0 +1,11 @@
function toNative(value) {
if (value === 'true' || value === 'false') {
return value === 'true';
} else if (!isNaN(+value)) {
return +value;
} else {
return value;
}
}
exports.toNative = toNative;