When it comes to web development, many frontend developers have experienced the waiting process for backend API integration. Initially, when the backend APIs are not ready, many developers are unable to start working and can only do some preliminary preparations, which can feel like a waste of time.
Some may use tool libraries to create mock APIs, with Mock.js being one of the most commonly used options. Mock.js is powerful and offers various functionalities, including the ability to easily launch a local server. However, it also has some limitations. For example, it cannot directly generate corresponding API documentation for the configured mock APIs, which requires manual checking and management, making it less convenient for maintenance and communication. Additionally, Mock.js has its own data generation syntax, which is convenient to use but comes with an additional learning curve and lacks flexibility.
To address these limitations, I would like to design a new tool with the following features:
- It should be able to generate various types of mock data using native JavaScript utility functions.
- While generating mock APIs, it should also generate corresponding API documentation. This way, we can understand the complete API through the documentation, making it convenient for both our development process and for the backend team to implement actual business logic based on the documentation.
Alright, let’s take a look at how we can step-by-step achieve our goal.
Building a Data Generation Function
The basic principle of generating mock data is to create corresponding data based on a description (referred to as a schema).
For example, let’s start with the simplest scenario:
const schema = {
name: 'Akira',
score: '100',
};
const data = generate(schema);
console.log(data);
In the given schema object, all the properties are constants. Therefore, the generated data will be the same as the original input, resulting in the following output:
{
"name":"akira",
"score":100
}
If we want to add some randomness to the generated data, we can use random functions. For example:
function randomFloat(from = 0, to = 1) {
return from + Math.random() * (to - from);
}
function randomInteger(from = 0, to = 1000) {
return Math.floor(randomFloat(from, to));
}
If we want to introduce some randomness in the generated data, we can use random functions. For example:
const schema = {
name: 'Akira',
score: randomInteger(),
}
...
Yes, it may seem simple at first glance, but in reality, it has limitations. Let’s continue and address those limitations.
Suppose we want to generate data in batches. In that case, we can design a repeat method that takes a schema as input and returns an array:
function repeat(schema, min = 3, max = min) {
const times = min + Math.floor((max - min) * Math.random());
return new Array(times).fill(schema);
}
es, with the repeat method in place, we can now use it to generate multiple data entries. For example:
const schema = repeat({
name: 'Akira',
score: randomInteger(),
}, 5);
When using the repeat method in its current form, the generated score values will be the same for all the entries because they are pre-generated before the repetition.
What should we do then?
Utilizing Function Lazy Evaluation
Let’s modify our generation function:
function randomFloat(from = 0, to = 1) {
return () => from + Math.random() * (to - from);
}
function randomInteger(from = 0, to = 1000) {
return () => Math.floor(randomFloat(from, to)());
}
The major change here is to modify the randomInteger generation function so that it doesn’t directly return a value but instead returns a function. This way, when we invoke it during the repeat process, we can obtain different random values.
To achieve this, our generator needs to be able to resolve and execute functions.
Here’s an implementation of the generator:
function generate(schema, extras = {}) {
if(schema == null) return null;
if(Array.isArray(schema)) {
return schema.map((s, i) => generate(s, {...extras, index: i}));
}
if(typeof schema === 'function') {
return generate(schema(extras), extras);
}
if(typeof schema === 'object') {
if(schema instanceof Date) {
return schema.toISOString();
}
if(schema instanceof RegExp) {
return schema.toString();
}
const ret = {};
for(const [k, v] of Object.entries(schema)) {
ret[k] = generate(v, extras);
}
return ret;
}
return schema;
};
Indeed, the generator is the core component of data generation, and you’ll find that it’s not inherently complex.
The key is to recursively handle different types of property values. When encountering a function, it is invoked to execute and return its result.
The following code shows how to generate five student records.
function generate(schema, extras = {}) {
if(schema == null) return null;
if(Array.isArray(schema)) {
return schema.map((s, i) => generate(s, {...extras, index: i}));
}
if(typeof schema === 'function') {
return generate(schema(extras), extras);
}
if(typeof schema === 'object') {
if(schema instanceof Date) {
return schema.toISOString();
}
if(schema instanceof RegExp) {
return schema.toString();
}
const ret = {};
for(const [k, v] of Object.entries(schema)) {
ret[k] = generate(v, extras);
}
return ret;
}
return schema;
};
function randomFloat(from = 0, to = 1) {
return () => from + Math.random() * (to - from);
}
function randomInteger(from = 0, to = 1000) {
return () => Math.floor(randomFloat(from, to)());
}
function genName() {
let i = 0;
return () => `student${i++}`;
}
function repeat(schema, min = 3, max = min) {
const times = min + Math.floor((max - min) * Math.random());
return new Array(times).fill(schema);
}
const res = generate(repeat({
name: genName(),
score: randomInteger(0, 100),
}, 5));
console.log(JSON.stringify(res, null, 2));
The data output is as follows:
[
{
"name": "student0",
"score": 47
},
{
"name": "student1",
"score": 71
},
{
"name": "student2",
"score": 68
},
{
"name": "student3",
"score": 96
},
{
"name": "student4",
"score": 91
}
]
Correct!
In summary, the key solution to address the issue is to utilize function expressions for lazy evaluation. This allows us to defer the evaluation of functions until the data generation process, ensuring that we obtain random values as needed to meet our requirements.
By using function expressions and resolving functions during the data generation, we can achieve the desired flexibility and randomness in our generated data.
Here is a complete example:
Generating API documentation
Generating API documentation based on the schema is indeed a crucial functionality. Essentially, it involves generating an HTML snippet from the schema. While the difficulty may not be significant, the details can be complex, and there are multiple optional approaches available.
For this purpose, I have chosen to generate a Markdown document from the schema and then use the marked library to parse it into HTML. Here’s an example of the initialization code for marked:
const renderer = new marked.Renderer();
renderer.heading = function(text, level, raw) {
if(level <= 3) {
const anchor = 'mockingjay-' + raw.toLowerCase().replace(/[^w\u4e00-\u9fa5]]+/g, '-');
return `<h${level} id="${anchor}"><a class="anchor" aria-hidden="true" href="#${anchor}"><svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg></a>${text}</h${level}>n`;
} else {
return `<h${level}>${text}</h${level}>n`;
}
};
const options = {
renderer,
pedantic: false,
gfm: true,
breaks: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false,
headerIds: false,
mangle: false,
};
marked.setOptions(options);
marked.use(markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
}));
Then, let’s prepare an HTML template:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AirCode Doc</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="A graphics system born for visualization.">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.css">
<link rel="stylesheet" href="https://unpkg.com/highlight.js@11.8.0/styles/github.css">
<style>
.markdown-body {
padding: 2rem;
}
</style>
</head>
<body>
<div class="markdown-body">
${markdownBody}
</div>
</body>
</html>
Finally, let’s implement a compile method:
compile() {
return async (params, context) => {
// console.log(process.env, params, context);
const contentType = context.headers['content-type'];
if(contentType !== 'application/json') {
context.set('content-type', 'text/html');
const markdownBody = marked.parse(this.info());
return await display(path.join(__dirname, 'index.html'), {markdownBody});
}
const method = context.method;
const headers = this.#responseHeaders[method];
if(headers) {
for(const [k, v] of Object.entries(headers)) {
context.set(k, v);
}
}
const schema = this.#schemas[method];
if(schema) {
return generate(schema, {params, context, mock: this});
}
if(typeof context.status === 'function') context.status(403);
else if(context.response) context.response.status = 403;
return {error: 'method not allowed'};
};
}
This method returns a serverless cloud function that returns different content based on the Content-Type of the HTTP request. If the Content-Type is application/json, it returns the generated JSON data for the API. Otherwise, it returns an HTML page. The this.info() function retrieves the Markdown code, and display renders the code using a template to generate the final API page.
Here’s an example of the generated API page:
That’s all the content. The complete code can be found in the code repository. If you are interested, you can try it out by yourself and contribute to the project through pull requests.
Thanks for reading.
Build a JavaScript Tool Library That Simulates Interfaces to Generate Random Data was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.