Motivation
- Prototyping a UI and want a quick way of testing API/CRUD functionalities?
- Working on a feature with external incomplete API dependencies?
- Ever wanted a very simple RESTful server with built-in CRUD operations?
If you answered yes to any of the questions stated above then mockfoundry may be right for you.
Goal
- To drastically reduce the boilerplate needed for prototyping with actual data.
- To enable true mocking-out of APIs and still achieving close to actual world behavior.
Installation
npm i --save mockfoundry // as a dependency
npm i --save-dev mockfoundry // as a development dependency. can be used for testing
Use Require
This library is meant to be run in Node.js and therefore is exported as a CommonJS library
const Mockfoundry = require('mockfoundry'); // require module
Port
Port is an essential part of mockfoundry as it is needed to create instances for the server. It is also used in the naming convention for the files in which data is saved.
If you start one instance on port 1000 and you start another instance on port 1001, I hope you would agree data saved on disk from those endpoints should be saved in their respective files. For that reason, mockfoundry treats ports as part of the data file name and maintains them accordingly,thus, mockfoundry fundamentally treats ports as databases.
The data will persist on disk even between restarts for the same port unless the override flag is set during instantiation so please tag ports accordingly.
Instance
const instance = new Mockfoundry(port [, override ]);
Run
const instance = new Mockfoundry(1000); // create instance on port 1000
instance.start(); // start server
API Primer
All APIs have a similar syntax convention:
host:port/action/collection?[optional-query-parameters]
Type | Meaning | Required | Default(s) |
---|---|---|---|
host | host of server | Yes | localhost (and is only localhost for now) |
port | port instance is running on | Yes | No default |
action | crud operation to be performed by the server | Yes | save, fetch, count, update, remove (and these are the only ones that can be used) |
collection | the type of data being saved. It is synonymous to a table in SQL or a type of document in a NoSQL | Yes | No Default |
optional-query-parameters | additional query parameters that can be used along with the fetch action to optimize the data returned | No | skip, limit, sort (and these are the only ones that can be used) |
HTTP Method Standards
Mockfoundry adheres to the HTTP standards with all API calls. Simply put, all actions use appropriate HTTP methods for respective calls.
Action | HTTP Method | Accepts Body | Requires Body | Required In Body | Optional In Body |
---|---|---|---|---|---|
save | POST | Yes | Yes | array of objects or an object | --- |
fetch | GET | No | --- | --- | --- |
fetch | POST | Yes | No | --- | fields and filters |
count | GET | No | --- | --- | --- |
count | POST | Yes | No | --- | filters |
update | PUT | Yes | Yes | filters and update | --- |
remove | DELETE | Yes | Yes | filters | --- |
APIs
Let's take a Vehicle Collection. We will proceed to learn how to do all CRUD operations in mockfoundry using our Vehicle Collection. Our Vehicle Collection has the following structure:
{
brand
model
year
kind
countryOfOrigin
spec {
engine
drivetrain
acceleration
speed
range
}
class
isElectric
isStreetLegal
features
safetyRating
awards
tags
}
The way in which we wrote out this collection will become particularly important later on. Other than that, it is a simple list of fields a Vehicle Collection can have.
All our examples will be explained using axios --- a promise based HTTP client for the browser and node.js.
Save API
At this point, mockfoundry does not enforce any kind of schema validation on a collection level. Anything you save in the collection will be what gets saved. Mockfoundry is first and foremost a mocking library and as such, we want to reduce the boilerplate setup needed to achieve just that.
For the sake of brevity and code readability, we will shorten what we show as being saved but in actuality, we are saving the whole structure described above.
Example --- Single Entry
axios.post(
'locahost:1000/save/vehicle',{
brand: 'bmw',
model: 'm4',
year: '2020',
...... // indicates more fields
}
)
.then(response => console.log(response)) // prints true if saved, else false
.catch(error => console.error(error)); // prints kind of error
Example --- Multiple Entry of the same collection
axios.post(
'locahost:1000/save/vehicle',[
{
brand: 'bmw',
model: 'm4',
year: '2020',
...... // indicates more fields
},
{
brand: 'toyota',
model: 'prius,
year: '2021',
...... // indicates more fields
}
]
)
.then(response => console.log(response)) // prints true if saved, else false
.catch(error => console.error(error)); // prints kind of error
Fetch API
Looking back at the API Primer and HTTP Method Standards, you will notice that the fetch action is a very special action. To reiterate, let's list all the combinations of things it can have:
- It can be called with or without a body
- It is the only action that accepts optional query parameters
- A new fun fact that was not apparent from before is, it can accept any arbitrary number of fields in exactly the structure described at the beginning of the APIs section. This way of requesting fields is inspired by the get-what-you-want model in graphql.
Example --- Simple Fetch
axios.get('locahost:1000/fetch/vehicle')
.then(data => console.log(data)) // prints an array of vehicle collection objects
.catch(error => console.error(error)); // prints kind of error
Example --- Fetch with Limit as Query Parameter
axios.get('locahost:1000/fetch/vehicle?limit=2')
.then(data => console.log(data)) // prints an array of two vehicle collection objects
.catch(error => console.error(error)); // prints kind of error
Example --- Fetch with Skip and Limit as Query Parameters
axios.get('locahost:1000/fetch/vehicle?skip=1&limit=2')
.then(data => {
// prints an array of two vehicle collection objects
// by skipping the first object it finds in the collection
console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
Example --- Fetch with Skip, Limit, and Sort as Query Parameters
axios.get('locahost:1000/fetch/vehicle?skip=1&limit=2&sort=year')
.then(data => {
// prints an array of two vehicle collection objects
// by skipping the first object it finds in the collection
// and sorting by year ascending order by default
console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
To specify a particular sort order, other than the default ascending, you need to set it with a colon character:
sort=year:desc
sort=spec.acceleration:asc
For multiple sorts, use the pipe character to list them out :
sort=year:desc|spec.acceleration:asc
sort=year|isElectric:desc
Example --- Fetch with Filter(s)
axios.post(
'locahost:1000/fetch/vehicle',{
"filters": [
{
"field": "kind",
"op": "$eq", // special operator character. $eq means equals to.
"value": "sedan"
}
]
}
)
.then(data => {
// prints an array of vehicle collection objects
// that matches the filter(s) passed in
console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
You can head over to the Filters section to learn how to use them. It is very straight forward.
Example --- Fetch with Field(s)
axios.post(
'locahost:1000/fetch/vehicle',{
"fields": `{
brand
model
year
kind
spec {
range
speed
engine
}
class
tags
}
`
}
)
.then(data => {
// prints an array of vehicle collection objects,
// wih only the fields listed in the fetch call
console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
A little about Fields
As mentioned earlier, the fetch action has special powers among which is the ability to list the fields you want. Thanks to ES6 string literals, we can list fields we want to return in a multiline format. It has many benefits such as, code readability, familiarity due to its object-like look and getting data in exactly the way fields are listed. Of course you can resort to using a single line string if the ES6 approach is not viable. The only issue with that is, you loose code readability and I know that will drive me nuts.
To maintain complaince with this structure, mockfoundry applies the following validation rules to a fields property:
- Needs to start with an opening curly brace ({).
- Needs to end with a closing curly brace (}).
- May contain spaces, tabs or new lines.
- Contents between opening and closing curly braces can only be any of the rules stated above along with alphanumerics, commas, and underscores.
Count API
The count action is similar to the fetch action but different in one obvious way --- it does not need fields or query parameters.
Example --- Simple Count
axios.get('locahost:1000/count/vehicle')
.then(data => console.log(data)) // prints the number of documents if any, zero if none
.catch(error => console.error(error)); // prints kind of error
Example --- Count with Filter(s)
axios.post(
'locahost:1000/count/vehicle',{
"filters": [
{
"field": "kind",
"op": "$eq", // special operator character. $eq means equals to.
"value": "sedan"
}
]
}
)
.then(data => {
// prints the number of documents
// that matches the filter(s) passed in if anym zero f none
console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
Update API
The update action is similar to the count action. However, it requires another property called update. I know, very original.
Since mockfoundry does not enforce any schema validation due to reasons stated earlier, the update action merges new fields with the rest of the collection.
NOTE:
Updates of inner objects must be done with a dot notation (parent_prop.child_prop) if you want to keep other values that might be in there. Otherwise, if you have all the required data on the client side, you can use the spread operator or any other merge solution to add new data in before updating.
Use the fetch action to select the fields you want to return.
Example --- Update
axios.put(
'locahost:1000/update/vehicle',{
"filters": [
{
"field": "kind",
"op": "$eq", // special operator character. $eq means equals to.
"value": "sedan"
}
],
"update": {
"year": 2021,
"spec.engine": "v6" // note dot notation
"spec.newProp: "some new value" // note dot notation
"anotherNewProp: {}
}
}
)
.then(response => console.log(response)) // prints true if updated, else false
.catch(error => console.error(error)); // prints kind of error
Remove API
The remove action is similar to the count action but different one obvious way --- it requires the filters property.
Example --- Remove with required Filter(s)
axios.delete(
'locahost:1000/remove/vehicle',{
"filters": [
{
"field": "kind",
"op": "$eq", // special operator character. $eq means equals to.
"value": "sedan"
}
]
}
)
.then(response => console.log(response)) // prints true if removed, else false
.catch(error => console.error(error)); // prints kind of error
Note that, mockfoundry treats empty filters array as match all. In that sense, an empty filters array is still an valid array and will cause all data of the passed collection to be removed. If that is your intention then ignore this caution.
Filters
All filter objects follow the field-op-value paradigm. That is because mockfoundry uses NeDB under the hood for database operations.
Besides the op property in a filter contstruction, the field and the value properties are self explanatory. The op property is either a logical or comparison operator. Below are the list of all supported operators:
Operator | Meaning | What question does it asks of the collection? | Value Type(s) |
---|---|---|---|
$eq | equal | for the given field, is any value equals what was passed? | string, number, regex, boolean, object, array |
$lt | less than | for the given field, is any value less than what was passed? | string, number |
$gt | greater than | for the given field, is any value greater than what was passed? | string, number |
$lte | less than or equal | for the given field, is any value less than or equal to what was passed? | string, number |
$gte | greater than or equal to | for the given field, is any value greater than or equal to what was passed? | string, number |
$neq | not equal | for the given field, is any value not equal to what was passed? | string, number, regex, boolean, object, array |
$exists | exists | for the given field, does the field exist (when value is true) or not exist (when value is false)? | boolean |
$regex | regex | for the given field, does any value match the regex expression passed? | string |
$not | not | for the given field, is the value passed not an exact match of anything in the collection? | string, number, boolean, object, array |
$in | in | for the given field, is the value passed a member of the array of values in the collection? | array of primitives or objects |
$nin | not in | for the given field, is the value passed not a member of the array of values in the collection? | array of primitives or objects |
$or | or | for the given field, is any of the array of values passed exactly a value in the collection? | string, number, boolean, object, array |
$and | and | for the given field, are all of the array of values passed exactly a value in the collection? | string, number, boolean, object, array |
Besides the $eq and $neq operators, the rest are direct replicas of what NeDB uses officially.
Error
Error is an unexpected but important part of any application. Mockfoundry uses fastify as its application server and draws on its error handling structure and even extends it where possible.
All errors in mockfoundry contain the following properties:
Property | Meaning | Always Present |
---|---|---|
statusCode | HTTP Code | Yes |
error | Type of error | Yes |
message | Reason for the error | Yes |
optionals | A spread of any aditional properties specific to the action being performed | No |
Besides the optionals property, all other properties are default and directly borrowed from fastify. The optionals property is an object that merges its contents with the defaults so there is no actual optionals property key in errors.
Issues
Please report issues as you see them during usage. It will help improve this library as a whole. Thank you.