Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@mikrokit/router
Advanced tools
Typescript RPC Like router with automatic Validation and Serialization
RPC Like router with automatic Validation and Serialization.
@mikrokit/router
🚀 Lightweight router based in plain javascript objects.
Thanks to it's RPC style there is no need to parse parameters or regular expressions when finding a route. Just a simple Map in memory containing all the routes.
MikroKit Router uses a Remote Procedure Call style routing, unlike traditional routers it does not use GET
, PUT
, POST
and DELETE
methods, everything should be transmitted using HTTP POST
method and all data is sent/received in the request/response body
and headers
.
HTTP
method is ignored by this router.body
and headers
. (no request params)JSON
format.RPC Like Request | REST Request | Description |
---|---|---|
POST /users/get BODY {"/users/get":[{"id":1}]} | GET /users/1 BODY NONE | Get user by id |
POST /users/create BODY {"/users/create":[{"name":"John"}]} | POST /users BODY {"name":"John"} | Create new user |
POST /users/delete BODY {"/users/delete":[{"id":1}]} | DELETE /users/1 BODY NONE | Delete user |
POST /users/getAll BODY {"/users/getAll":[]} | GET /users BODY NONE | Get All users |
Please have a look to this great Presentation for more info about each different type of API and the pros and cons of each one:
Nate Barbettini – API Throwdown: RPC vs REST vs GraphQL, Iterate 2018
Routes
A route is just a function, the first parameter is always the call context
, the rest of parameters are extracted from the request body or headers. Route names are defined using a plain javascript object, where every property of the object is the route's name. Adding types is recommended when defining a route so typescript can statically check parameters.
MikroKit only cares about the path
, and completely ignores the http method, so in theory request could be made using POST
or PUT
, or the router could be used in any event driven environment where the concept of method does not exist.
// examples/routes-definition.routes.ts
import {Route, Handler, Routes, MkRouter} from '@mikrokit/router';
const sayHello: Handler = (context, name: string): string => {
return `Hello ${name}.`;
};
const sayHello2: Route = {
route(context, name1: string, name2: string): string {
return `Hello ${name1} and ${name2}.`;
},
};
const routes: Routes = {
sayHello, // api/sayHello
sayHello2, // api/sayHello2
};
MkRouter.setRouterOptions({prefix: 'api/'});
MkRouter.addRoutes(routes);
Using javascript names helps keeping route names simple, it is not recommended to use the array notation to define route names. no url decoding is done when finding the route
// examples/no-recommended-names.routes.ts
import {Routes, MkRouter, Route} from '@mikrokit/router';
const sayHello: Route = (context, name: string): string => {
return `Hello ${name}.`;
};
const routes: Routes = {
'say-Hello': sayHello, // api/say-Hello !! NOT Recommended
'say Hello': sayHello, // api/say%20Hello !! ROUTE WONT BE FOUND
};
MkRouter.addRoutes(routes);
Route parameters
are passed as an Array in the request body, in a field with the same name as the route. Elements in the array must have the same order as the function parameters.
Route response
is send back in the body in a field with the same name as the route.
The reason for this weird naming is to future proof the router to be able to accept multiple routes on a single request. However this can be changed setting the routeFieldName
in the router options.
POST REQUEST | Request Body | Response Body |
---|---|---|
/api/sayHello | {"/api/sayHello": ["John"] } | {"/api/sayHello": "Hello John."} |
/api/sayHello2 | {"/api/sayHello2": ["Adan", "Eve"] } | {"/api/sayHello2": "Hello Adan and Eve."} |
Hooks
A route might require some extra data like authorization, preconditions, logging, etc... Hooks are auxiliary functions executed in order before or after the route.
Hooks can use context.shared
to share data with other routes and hooks. The return value will be ignored unless canReturnData
is set to true, in that case the returned value will be serialized in and send back in response body.
// examples/hooks-definition.routes.ts
import {Route, Routes, MkRouter, Hook} from '@mikrokit/router';
import {getAuthUser, isAuthorized} from 'MyAuth';
const authorizationHook: Hook = {
fieldName: 'Authorization',
inHeader: true,
async hook(context, token: string): Promise<void> {
const me = await getAuthUser(token);
if (!isAuthorized(me)) throw {code: 401, message: 'user is not authorized'};
context.auth = {me}; // user is added to context to shared with other routes/hooks
},
};
const getPet: Route = async (context, petId: number): Promise<Pet> => {
const pet = context.app.deb.getPet(petId);
// ...
return pet;
};
const logs: Hook = {
async hook(context): Promise<void> {
const me = context.errors;
if (context.errors) await context.cloudLogs.error(context.errors);
else context.cloudLogs.log(context.request.path, context.auth.me, context.mkkOutput);
},
};
const routes: Routes = {
authorizationHook, // header: Authorization (defined using fieldName)
users: {
getPet,
},
logs,
};
MkRouter.addRoutes(routes);
Execution Order
The order in which routes
and hooks
are added to the router is important as they will be executed in the same order they are defined (Top Down order). An execution path is generated for every route.
// examples/correct-definition-order.routes.ts#L12-L26
const routes: Routes = {
authorizationHook, // hook
users: {
userOnlyHook, // hook
getUser, // route: users/getUser
},
pets: {
getPet, // route: users/getUser
},
errorHandlerHook, // hook,
loggingHook, // hook,
};
MkRouter.addRoutes(routes);
users/getUser
graph LR;
A(authorizationHook) --> B(userOnlyHook) --> C{{getUser}} --> E(errorHandlerHook) --> D(loggingHook)
pets/getPets
graph LR;
A(authorizationHook) --> B{{getPet}} --> E(errorHandlerHook) --> C(loggingHook)
To guarantee the correct execution order of hooks and routes, the properties of the router CAN NOT BE numeric or digits only.
An error will thrown when adding routes with MkRouter.addRoutes
. More info about order of properties in javascript objects here and here.
// examples/correct-definition-order.routes.ts#L27-L40
const invalidRoutes = {
authorizationHook, // hook
1: {
// invalid (this would execute before the authorizationHook)
getFoo, // route
},
'2': {
// invalid (this would execute before the authorizationHook)
getBar, // route
},
};
MkRouter.addRoutes(invalidRoutes); // throws an error
Throwing errors within Routes & Hooks
All errors thrown within Routes/Hooks will be automatically catch and handled, as there is no concept of logger within the router, to errors are generated, One public error to be returned in the context.response.body
& one private error stored in the context.internalErrors
. The public errors only contains generic message an an status code, the private errors contains also stack trace and the rest of properties of any js Error.
For proper standardization of errors it is recommended to always throw a RouteError
, that contains an statusCode
and both normal error.message
as well as a error.publicMessage
.
// examples/error-handling.routes.ts
import {Route, RouteError, StatusCodes} from '@mikrokit/router';
const getSomeData: Route = {
route: (context): void => {
try {
const data = context.app.db.getSomeData();
} catch (dbError) {
const statusCode = StatusCodes.INTERNAL_SERVER_ERROR;
const publicMessage = `Cant fetch data.`;
// Only statusCode and publicMessage will be returned in the response.body
/*
Full RouteError containing dbError message and stacktrace will be added
to context.internalErrors, so it can be logged or managed after
*/
throw new RouteError(statusCode, publicMessage, dbError);
}
},
};
Routes & Hooks Config
Hooks config | Routes config |
---|---|
|
|
Your application might need to add some extra metadata to every route or hook, to keep types working you can extend the Route
and Hook
types as follows:
// examples/extending-routes-and-hooks.routes.ts
import {Route, Hook} from '@mikrokit/router';
type MyRoute = Route & {doNotFail: boolean};
type MyHook = Hook & {shouldLog: boolean};
const someRoute: MyRoute = {
doNotFail: true,
route: (): void => {
if (someRoute.doNotFail) {
// do something
} else {
throw {statusCode: 400, message: 'operation failed'};
}
},
};
const someHook: MyHook = {
shouldLog: false,
hook: (context): void => {
if (someHook.shouldLog) {
context.app.cloudLogs.log('hello');
} else {
// do something else
}
},
};
Call Context
The Context
or Call Context
contains all data related to the route being called and is always passed in the first parameter to routes/hooks handler.
Most of the data within the Context
is marked as read only, this is because it is not recommended modifying the context manually. Instead Hooks/Routes should just return data, modify the shared
object or throw a RouteError
and the router would take care of correctly assign values to the Call Context
. It is still possible to modify them (the context is not a real Immutable js object).
// src/types.ts#L159-L204
export type MkResponse = {
statusCode: Readonly<number>;
/** response errors: empty if there were no errors during execution */
errors: Readonly<PublicError[]>;
/** response headers */
headers: MkHeaders;
/** the router response data, JS object */
body: Readonly<MapObj>;
/** json encoded response, contains data and errors if there are any. */
json: Readonly<string>;
};
/** The call Context object passed as first parameter to any hook or route */
export type Context<
App,
SharedData,
ServerReq extends MkRequest,
AnyServerCall extends ServerCall<ServerReq> = ServerCall<ServerReq>,
> = Readonly<{
/** Static Data: main App, db driver, libraries, etc... */
app: Readonly<App>;
serverCall: Readonly<AnyServerCall>;
/** Route's path */
path: Readonly<string>;
/**
* list of internal errors.
* As router has no logging all errors are stored here so can be managed in a hook or externally
*/
internalErrors: Readonly<RouteError[]>;
/** parsed request.body */
request: Readonly<{
headers: MapObj;
body: MapObj;
}>;
/** returned data (non parsed) */
response: Readonly<MkResponse>;
/** shared data between route/hooks handlers */
shared: Readonly<SharedData>;
}>;
export type ServerCall<ServerReq extends MkRequest> = {
/** Server request
* i.e: '@types/aws-lambda/APIGatewayEvent'
* or http/IncomingMessage */
req: ServerReq;
};
// examples/using-context.routes.ts
import {MkRouter, Context} from '@mikrokit/router';
import {APIGatewayProxyResult, APIGatewayEvent} from 'aws-lambda';
import {someDbDriver} from 'someDbDriver';
import {cloudLogs} from 'MyCloudLogLs';
const app = {cloudLogs, db: someDbDriver};
const shared = {auth: {me: null}};
const getSharedData = (): typeof shared => shared;
type App = typeof app;
type SharedData = ReturnType<typeof getSharedData>;
type CallContext = Context<App, SharedData, APIGatewayEvent>;
const getMyPet = async (context: CallContext): Promise<Pet> => {
// use of context inside handlers
const user = context.shared.auth.me;
const pet = context.app.db.getPetFromUser(user);
context.app.cloudLogs.log('pet from user retrieved');
return pet;
};
const routes = {getMyPet};
MkRouter.initRouter(app, getSharedData);
MkRouter.addRoutes(routes);
Automatic Serialization and Validation
Mikrokit uses Deepkit's runtime types to automatically validate request params and serialize/deserialize request/response data.
Thanks to Deepkit's magic the type information is available at runtime and the data can be auto-magically Validated and Serialized. For more information please read deepkit's documentation:
Code | POST Request /users/getUser |
---|---|
|
|
Deepkit does not support Type Inference, parameter types
and more importantly return types
must be explicitly defined, so they are correctly validated/serialized.
🚫 Invalid route definitions!
const myRoute1: Route = () {};
const myRoute2: Route = () => null;
const sayHello: Route = (context, name) => `Hello ${name}`;
const getYser: Route = async (context, userId) => context.db.getUserById(userId);
✅ Valid route definitions!
const myRoute1: Route = (): void {};
const myRoute2: Route = (): null => null;
const sayHello: Route = (context: Context, name:string): string => `Hello ${name}`;
const getYser: Route = async (context: Context, userId:number): Promise<User> => context.db.getUserById(userId);
Declaring explicit types everywhere can be a bit annoying, so you could suffix your route filles with .routes.ts
and add bellow eslint config to your project, (the important part here is the overrides
config).
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parserOptions: {
project: ['./tsconfig.json', './packages/*/tsconfig.json'],
},
overrides: [
{
files: ['**/*.routes.ts'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'error',
},
},
],
};
Router Options
// src/constants.ts#L37-L85
export const DEFAULT_ROUTE_OPTIONS: Readonly<RouterOptions> = {
/** prefix for all routes, i.e: api/v1.
* path separator is added between the prefix and the route */
prefix: '',
/** suffix for all routes, i.e: .json.
* Not path separators is added between the route and the suffix */
suffix: '',
/** Transform the path before finding a route */
pathTransform: undefined,
/** configures the fieldName in the request/response body used for a route's params/response */
routeFieldName: undefined,
/** Enables automatic parameter validation */
enableValidation: true,
/** Enables automatic serialization/deserialization */
enableSerialization: true,
/**
* Deepkit Serialization Options
* loosely defaults to false, Soft conversion disabled.
* !! We Don't recommend to enable soft conversion as validation might fail
* */
serializationOptions: {
loosely: false,
},
/**
* Deepkit custom serializer
* @link https://docs.deepkit.io/english/serialization.html#serialisation-custom-serialiser
* */
customSerializer: undefined,
/**
* Deepkit Serialization Options
* @link https://docs.deepkit.io/english/serialization.html#_naming_strategy
* */
serializerNamingStrategy: undefined,
/** Custom body parser, defaults to Native JSON */
bodyParser: JSON,
/** Response content type.
* Might need to get updated if the @field bodyParser returns anything else than json */
responseContentType: 'application/json; charset=utf-8',
};
Full Working Example
// examples/full-example.routes.ts
import {MkRouter, Context, Route, Routes, Hook, MkError, StatusCodes} from '@mikrokit/router';
import {APIGatewayEvent} from 'aws-lambda';
interface User {
id: number;
name: string;
surname: string;
}
type NewUser = Omit<User, 'id'>;
const myDBService = {
usersStore: new Map<number, User>(),
createUser: (user: NewUser): User => {
const id = myDBService.usersStore.size + 1;
const newUser: User = {id, ...user};
myDBService.usersStore.set(id, newUser);
return newUser;
},
getUser: (id: number): User | undefined => myDBService.usersStore.get(id),
updateUser: (user: User): User | null => {
if (!myDBService.usersStore.has(user.id)) return null;
myDBService.usersStore.set(user.id, user);
return user;
},
deleteUser: (id: number): User | null => {
const user = myDBService.usersStore.get(id);
if (!user) return null;
myDBService.usersStore.delete(id);
return user;
},
};
// user is authorized if token === 'ABCD'
const myAuthService = {
isAuthorized: (token: string): boolean => token === 'ABCD',
getIdentity: (token: string): User | null => (token === 'ABCD' ? ({id: 0, name: 'admin', surname: 'admin'} as User) : null),
};
const app = {
db: myDBService,
auth: myAuthService,
};
const shared = {
me: null as any as User,
};
const getSharedData = (): typeof shared => shared;
type App = typeof app;
type SharedData = ReturnType<typeof getSharedData>;
type CallContext = Context<App, SharedData, APIGatewayEvent>;
const getUser: Route = (ctx: CallContext, id: number): User => {
const user = ctx.app.db.getUser(id);
if (!user) throw {statusCode: 200, message: 'user not found'};
return user;
};
const createUser: Route = (ctx: CallContext, newUser: NewUser): User => ctx.app.db.createUser(newUser);
const updateUser: Route = (ctx: CallContext, user: User): User => {
const updated = ctx.app.db.updateUser(user);
if (!updated) throw {statusCode: 200, message: 'user not found, can not be updated'};
return updated;
};
const deleteUser: Route = (ctx: CallContext, id: number): User => {
const deleted = ctx.app.db.deleteUser(id);
if (!deleted) throw {statusCode: 200, message: 'user not found, can not be deleted'};
return deleted;
};
const auth: Hook = {
inHeader: true,
fieldName: 'Authorization',
hook: (ctx: CallContext, token: string): void => {
const {auth} = ctx.app;
if (!auth.isAuthorized(token)) throw {statusCode: StatusCodes.FORBIDDEN, message: 'Not Authorized'} as MkError;
ctx.shared.me = auth.getIdentity(token) as User;
},
};
const routes: Routes = {
auth,
users: {
get: getUser, // api/v1/users/get
create: createUser, // api/v1/users/create
update: updateUser, // api/v1/users/update
delete: deleteUser, // api/v1/users/delete
},
};
MkRouter.initRouter(app, getSharedData, {prefix: 'api/v1'});
MkRouter.addRoutes(routes);
MIT LICENSE
FAQs
Typescript RPC Like router with automatic Validation and Serialization
The npm package @mikrokit/router receives a total of 0 weekly downloads. As such, @mikrokit/router popularity was classified as not popular.
We found that @mikrokit/router demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.