README
Client Side Routes Manager, enables route validation, completion and definition. enables creating a quick automatic mock, using the defined routes.
Installation
$ npm install auto-route-creator --save
Basic Usage
a more comprehensive example can be found in example folder
Route Definition
import { RouteCreator } from "auto-route-creator";
let { METHOD, SYMBOLS, getRoute } = RouteCreator;
let { PROTOCOL, DYNAMIC, QUERY, NAME } = SYMBOLS;
const DKEY_CAR_ID = "CID";
const DKEY_D_ID = "DID";
//Define your server routes as a tree
const MY_ROUTES = {
cars: {
[PROTOCOL]: METHOD.GET,
[QUERY]: ["limit", "startFrom", "model"], // Query param
id: {
[DYNAMIC]: DKEY_CAR_ID, // Marks this as a dynamic part of route
drivers: {
[PROTOCOL]: METHOD.POST,
id: {
//Note that when no protocol is defined GET is assumed
[DYNAMIC]: DKEY_D_ID
}
},
}
}
}
RouteCreator.initRoutes(MY_ROUTES)
export { MY_ROUTES };
//// Example usegae (this would be placed in logical locations in your app)
let CAR_ROUTE = getRoute(MY_ROUTES.cars.id, { pathArgs: { [DKEY_CAR_ID]: "1234" } });
fetchExample(CAR_ROUTE).then(car=>console.log());
function fetchExample(route) {
const BASE_API = "http://localhost:3004";
const URL = BASE_API + route.path();
let fetchOptions = {
method: route.protocol,
body: route.body
}
return fetch(URL, fetchOptions).then(rj => rj.json());
}
one major advantage defining routes this way is we get auto complete, and refactoring is extremely easy. i.e. imagine we decide to move the drivers path up one level so it is not under a specific car id.
If routes were defined in a non tree way, you would have to change all dependent routes. in our example we would simply cut and paste and get
const MY_ROUTES = {
cars: {
[PROTOCOL]: METHOD.GET,
[QUERY]: ["limit", "startFrom", "model"], // Query param
drivers: {
[PROTOCOL]: METHOD.POST,
id: {
//Note that when no protocol is defined GET is assumed
[DYNAMIC]: DKEY_D_ID
}
},
id: {
[DYNAMIC]: DKEY_CAR_ID, // Marks this as a dynamic part of route
}
}
}
as all usage depends on structure, you would get an error areas where previous path was used and not updated, making sure you do not forget to update. even better, if you have a refactor tool the change is seamless.
Automatic Mock Definition
with a few additions, you can get an automatic mock server filled with random values based on your route definition.
import { AutoMock, RouteCreator } from "auto-route-creator";
let { BIND, SYMBOLS } = RouteCreator;
let { setMockData, getMockData } = AutoMock;
const { RESPONSE } = SYMBOLS;
const MOCK_RESPONSE_DEFINITION = {
cars: {
[RESPONSE]: {
template: {
cars: [
{
id: "{{datatype.uuid}}:string",
manufacturer: "{{vehicle.manufacturer}}:string",
model: "{{vehicle.model}}:string",
condition: "[Excellent|Very Good|Good|Fair]:string",
}
]
},
dynamicKeys: [BIND(DKEY_CAR_ID, "id")]
},
id: {
[RESPONSE]: {
template: {
color:"[blue|white|black]:string",
//Static - will be evaluated once
year: (url)=>"200"+Math.floor(Math.random()*10),
//Dynamic - will be evaluated at runtime
speed: (url)=>(req, data, proto)=>Math.floor(Math.random()*100)
}
},
}
}
};
//usually in a separate location join the 2 definitions (ROUTES + MOCK)
const MOCK_JOINED_DEF = RouteCreator.joinResponseRoutes(MY_ROUTES, MOCK_RESPONSE_DEFINITION);
//Run Mock server
AutoMock.mock(MOCK_JOINED_DEF,{port:3004});
as you can see above there are various ways to create mock data, simplest is by utilizing the random values generator faker-js under the hood to parse string and create random values. i.e. in the above example we used
cars: [
{
id: "{{datatype.uuid}}:string",
...
}
]
in the above example, cars gets an array, telling the mock server this is an array value, the object inside the array is what will fill the array.
In the object above we mock the car object, focusing on the object.id
, its value is generated with the following pattern
{ id: "{{datatype.uuid}}:string" }
where "{{datatype.uuid}}"
part is the pattern for generating random uuid (uses faker-js syntax) and string
marks the data type to convert into.
note you can also generate your own values using a function instead of a string value. i.e.
year: (url)=>"200"+Math.floor(Math.random()*10), // will be generated once on bring up
By returning another function from that function you can generate values at run time. To learn more read below
year: ()=>()=>"200"+Math.floor(Math.random()*10), // will be regenerated on every request
Defining Request Routes
Define your routes by creating a Object representing routes as a tree
i.e. if you have the following route /cars/:id/speed
it will be represented as
let ROUTES = {
cars:{
id:{
[DYNAMIC]: "cid", //more on this below
speed:{
// route definitions
}
}
}
}
adding a different route /cars/:id/passenger
requires only to add another child under id.
by using the following symbols, inside each paths object you can describe the request as follows:
Request Symbols | Definition |
---|---|
PROTOCOL | used to define route protocol (defaults to GET, use RouteCreator.METHOD for enums) |
DYNAMIC | a string value that marks this part of route as dynamic, i.e. /cars/:id the id part is dynamic and should be supplied for request, if not an error will be issued. the value should be a unique name used to reference this id later on |
NAME | a string value used to change the default name of the path part, which is the path object key |
QUERY | used to define the request query param keys, if wrong keys supplied a warning is issued, can receive a string array (i.e. ["limit","StartFrom"] ) defining the query keys, or a function for any custom validation or processing you might require (see example) |
BODY | function given here will be used on supplied body and replace route body with its return value |
once definition is complete, routes should be initialized using the init function initRoutes(ROUTES)
note this function works in place and does not return a value, this is to alow the definition object to retain its structure and features such as refactor auto complete and such.
A full example of routes here
Using request routes definition
once you have routes defined and initialized you can use them to generate your request path and headers by calling the RouteCreator.getRoute
function
for example
let options = {
pathArgs: { "cid": "mike" }, // Note "cid" is the DYNAMIC name we gave earlier in route definition
queryParams: [2, 0, true]
};
let route = getRoute(ROUTES.cars.id.checkups, options);
// /cars/mike?carsLimit=10&carsType=subaru&carsColor=red
getRoute API
let route = getRoute(routeObject,Options)
gets the route object of the given path and options.
arguments
- routeObject - is a path in the previously defined ROUTE object.
- Options - an object in the following format
{pathArgs:..., queryParams:...,bodyParams:,...}
where:pathArgs
- holds the paths dynamic parts values, should be an object with the key matching the name given in the[DYNAMIC]
part of the route and the value the path value. i.e.{cid:1234}
will replace the car id part of the route with1234
queryParams
- hold the query param values can be an object with key/value matching the query param name and value or an array of values fitting query param declaration order- bodyParams - holds body of message, can be in any format, if
[BODY]
was supplied in route, this value will be passed through the[BODY]
function first.
return value
object representing the route and all its data object has following properties:
route.path()
- returns the string representation of the path with query params and path partsroute.body
- request bodyroute.protocol
- request protocol
usage Example
// route is defined and supplied with path argument
// if argument was missing a warning would be issued
let CAR_ROUTE = getRoute(MY_ROUTES.cars.id, { pathArgs: { [DKEY_CAR_ID]: "1234" } });
//fetch wrapper
fetchExample(CAR_ROUTE).then(car=>console.log());
//function takes the route object and constructs a fetch request
function fetchExample(route) {
const BASE_API = "http://localhost:3004";
const URL = BASE_API + route.path();
let fetchOptions = {
method: route.protocol,
body: route.body
}
return fetch(URL, fetchOptions).then(rj => rj.json());
}
Mock definition
Bu default you can run mock as is on all routes, you will however receive a message for every route that no definition was supplied, to define return values, you can add definitions on the routes object itself, which is not recommended as you want to separate routes definition from the mock definition.
see full usage example here