Create json-server with multiple files

繁體中文 | English

# Intro

You can find a template here. (opens new window)

I'm going to share how I work with json-server, but perhaps there's another way you work more comfortable.

# What is json-server?

First, let's take a look at json-server#getting-started (opens new window) :

Create a db.json file with some data

{
  "posts": [
    { "id": 1, "title": "json-server", "author": "typicode" }
  ],
  "comments": [
    { "id": 1, "body": "some comment", "postId": 1 }
  ],
  "profile": { "name": "typicode" }
}

Start JSON Server

json-server --watch db.json

Now if you go to http://localhost:3000/posts/1, you'll get

{ "id": 1, "title": "json-server", "author": "typicode" }

It's pretty simple. However, the data might be more complicated in real world, so I'm going to create a file for each api.

# Starting

# Structure with multiple files

Create a new folder mock, and add a script in package.json:

"scripts": {
  "mock": "json-server --watch ./mock/db.js"
}

First, transform db.json into db.js:

// db.js
// Should export a function which return an object
module.exports = () => ({
  posts: [{ id: 1, title: "json-server", author: "typicode" }],
  comments: [{ id: 1, body: "some comment", postId: 1 }],
  profile: { name: "typicode" }
});

Since it's no longer a json file, you can load data from separate files:

// db.js
const posts = require("./posts");
const comments = require("./comments");
const profile = require("./profile");
module.exports = () => ({
  posts,
  comments,
  profile
});

Create corresponding files, for example:

// posts.js
module.exports = [{ id: 1, title: "json-server", author: "typicode" }];

# Import files automatically

It's not very handy that you have to import the file whenever you create one.

Let's leverage glob (opens new window) to get file names so that we can load files dynamically:

// db.js
const Path = require("path");
const glob = require("glob");
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/*.js", {
  nodir: true
});
// apiFiles will be :
// [ '/Users/billy/Desktop/json-server-multiple-files-sample/mock/comments.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/db.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/posts.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/profile.js' ]

let data = {};

apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split("mock/"); // e.g. comments.js
  url = url.slice(0, url.length - 3)  // remove .js >> comments
  data[url] = api;
});

// data will be :
// { 'comments': [ { id: 1, body: 'some comment', postId: 1 } ],
//   'db': {},
//   'posts': [ { id: 1, title: 'json-server', author: 'typicode' } ],
//   'profile': { name: 'typicode' } }

module.exports = () => {
  return data;
};

But we don't need to load db.js to be a API, right?
Rename db.js > _db.js and add a rule to prevent all files with _ prefix from transforming API routes:

// db.js
// ...
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/[!_]*.js", {
// ...

# Monitor multiple files

Since json-server will automatically restart only when _db.js is changed, I have to do it whenever adding a API or modify data. Solution (opens new window):

yarn add -D nodemon
"scripts": {
  "mock": "nodemon --watch mock --exec 'json-server ./mock/_db.js'"
}

# Better router

What if APIs become more complicated:

/blog/posts
/blog/comments
/blog/profile
/documents/query

I wish I can have the mock folder structure like below:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
└── documents
    └── query.js

Modify rules of _db.js:

// db.js
const Path = require('path');
const glob = require('glob');
const config = require('./config.json');
const apiFiles = glob.sync(
  Path.resolve(__dirname, './') + '/**/[!_]*.js',
  {
    nodir: true,
  },
);

let data = {};


apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split('mock/');
  url = url.slice(0, url.length - 3);
  data[url.replace(/\//g, '-')] = api; // the only change
});

// data will be something like:
// { 'blog-comments': [ { id: 1, body: 'some comment', postId: 1 } ],
//   'blog-posts': [ { id: 1, title: 'json-server', author: 'tydpicode' } ],
//   'blog-profile': { name: 'typicode' },
//   'documents-query': { data: 123 } }

module.exports = () => {
  return data;
};

Create config.json and router.json files: (config usage (opens new window)router usage (opens new window)

// config.json
{
  "port": 9898,
  "routes": "./mock/router.json"
}
// router.json
{
  "/*/*/*": "/$1-$2-$3",
  "/*/*": "/$1-$2"
}

Now /documents/query will correspond to /documents-query.
If /documents/query/something is existed, it'll correspond to /documents-query-something. That's totally handle the folder structure.

Last modified:

// _db.js
const Path = require("path");
const glob = require("glob");
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/[!_]*.js", {
  nodir: true
});

let data = {};
apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split("mock/");
  url =
    url.slice(url.length - 9) === "/index.js"
      ? url.slice(0, url.length - 9) // remove /index.js
      : url.slice(0, url.length - 3); // remove .js
  data[url.replace(/\//g, "-")] = api;
});
module.exports = () => {
  return data;
};

Now if there's a API route /documents, I can restructure

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   └── query.js
├── documents.js
└── router.json

to be something like:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   ├── index.js
│   └── query.js
└── router.json

# Others

# Decode Chinese characters

If there's a API route documents/基本, I won't be able to visit it. 基本will be encode to be%E5%9F%BA%E6%9C%AC. Since we know the encoded string is consisted of the percent character "%" followed by two hexadecimal digits, I can create a middleware to handle it. Use prefix _ which I've reserved. Decode url when the url contains the encoded string:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   ├── index.js
│   ├── query.js
│   └── 基本.js
├── middlewares
│   ├── _decodeChinese.js
└── router.json
// _decodeChinese.js
module.exports = function(req, res, next) {
  const regex = /%(\d|[A-Z]){2}/;
  const isMatch = regex.test(req.url);
  if (isMatch) {
    req.url = decodeURI(req.url);
  }
  next();
};
// config.json
{
  // ...
  "middlewares": ["./mock/middlewares/_decodeChinese.js"]
  // ...
}

# Get data using POST

Sometimes People have to get data by POST instead of GET, but custom route of json-server will always be GET. Solution (opens new window)
Again middleware. To detect whenever the method is POST, transform it into GET.

// _postAsGet.js
module.exports = function(req, res, next) {
  if (req.method === 'POST') {
    // Converts POST to GET and move payload to query params
    // This way it will make JSON Server that it's GET request
    req.method = 'GET';
    req.query = req.body;
  }
  // Continue to JSON Server router
  next();
};
// config.json
{
  // ...
  "middlewares": ["./mock/middlewares/_postAsGet.js"]
  // ...
}

# Run App and mock server at the same time in one terminal

It's kind of annoyed to open two terminal to run each server. Actually, I can just run yarn mock & yarn start. But how do I know where logs come from ?
I leverage Concurrently (opens new window) to solve this problem:

yarn add -D concurrently
"scripts": {
  "dev": "concurrently \"yarn:start\" \"yarn:mock\"",
}

End.