KoaSmart is a framework based on Koajs2, which allows you to develop RESTful APIs with : Class, Decorator, Params checker
A framework based on Koajs2 with Decorator, Params checker and a base of modules (cors
, bodyparser
, compress
, I18n
, etc... ) to allow you to develop a smart api easily
export default class RouteUsers extends Route {
// get route: http://localhost:3000/users/get/:id
@Route.Get({
path: 'get/:id'
})
async get(ctx) {
const user = await this.models.users.findById(ctx.params.id);
this.assert(user, 404, ctx.state.__('User not found'));
this.sendOk(ctx, user);
}
// post route: http://localhost:3000/users/add
@Route.Post({
accesses: [Route.accesses.public],
params: { // params to allow: all other params will be rejected
email: true, // return an 400 if the body doesn't contain email key
name: false,
},
})
async add(ctx) {
const body = this.body(ctx); // or ctx.request.body
// body can contain only an object with email and name field
const user = await this.models.user.create(body);
this.sendCreated(ctx, user);
}
}
- What's in this framework ?
- Install
- Router with decorator
- Params checker of POST body
- Get Started
- Add treatment on route
**This framework gives you the tools to use a set of modules: **
- For routing
koajs 2
as the main, underlying frameworkkcors
is used to handle cross-domain requestskoa2-ratelimit
To limit bruteforce requestskoa-helmet
helps you secure your apikoa-bodyparser
to parse request bodieskoa-compress
to compress the responsekoa-i18n
for Internationalization (I18n)
@Decorators
to ensure a better project structuremoment
Parse, validate, manipulate, and display dates in javascript.lodash
A modern JavaScript utility library delivering modularity, performance, & extrasjsonwebtoken
an implementation of JSON Web Tokens JWT
the full documentation for this module can be found here
npm install koa-smart
All routes have to extend the Route
class in order to be mount
-
Prefix of routes
If you have a route class with the name
RouteMyApi
, all the routes inside said class will be preceded by/my-api/
-
How does it work ?
- the
Route
word is removed - uppercase letters are replaced with '-'. (essentially converting camelCase into camel-case) e.g.: this will add a get route => http://localhost:3000/my-api/hello
export default class RouteMyApi extends Route { @Route.Get({}) async hello(ctx) { this.sendOk(ctx, ctx.state.__('hello')); } }
- the
-
Change prefix of all routes in the class: http://localhost:3000/my-prefix/hello
@Route.Route({ routeBase: 'my-prefix', }) export default class RouteMyApi extends Route { @Route.Get({}) async hello(ctx) { this.sendOk(ctx, ctx.state.__('hello')); } }
-
-
Get route http://localhost:3000/my-api/hello
@Route.Get({}) async hello(ctx) { this.sendOk(ctx, null, ctx.state.__('hello')); }
-
Change path http://localhost:3000/my-api/myroute/15
@Route.Get({ path: '/myroute/:id' }) async hello(ctx) { this.sendOk(ctx, ctx.state.__('hello') + ctx.params.id); }
-
Post route http://localhost:3000/my-api/user-post
@Route.Post({ params: { // params to allow: all other params will be rejected email: true, // return a 400 error if the body doesn't contain email key name: false, // optional parameter }, }) async userPost(ctx) { const body = this.body(ctx); // body can contain only an object with email and name field const user = await this.models.user.create(body); this.sendCreated(ctx, user); }
-
Disable route
- Disable all routes in a class
to disable all routes in a class you should add
disable
in the content of your decorator class@Route.Route({ disable: true, }) export default class RouteMyApi extends Route { // All routes in this class will not be mounted }
- Disable a specific route
to disable a specific route you can add
disable
in the content of your decorator@Route.Get({ disable: true, // this route will not be mounted }) async hello(ctx) { this.sendOk(ctx, null, ctx.state.__('hello')); }
-
RateLimit : For more infos, see the
koa2-ratelimit
module-
Configure
import { App } from 'koa-smart'; import { RateLimit, RateLimitStores } from 'koa-smart/middlewares'; const app = new App({ port: 3000 }); // Set Default Option const store = new RateLimitStores.Memory() OR new RateLimitStores.Sequelize(sequelizeInstance) RateLimit.defaultOptions({ message: 'Too many requests, get out!', store: store, // By default it will create MemoryStore }); // limit 100 accesses per min on your API app.addMiddlewares([ // ... RateLimit.middleware({ interval: { min: 1 }, max: 100 }), // ... ]);
-
RateLimit On Decorator
Single RateLimit
@Route.Get({ // allow only 100 requests per day to /view rateLimit: { interval: { day: 1 }, max: 100 }, }) async view(ctx) { this.sendOk(ctx, null, ctx.state.__('hello')); }
Multiple RateLimit
// Multiple RateLimit @Route.Get({ rateLimit: [ { interval: { day: 1 }, max: 100 }, // allow only 100 requests per day { interval: { min: 2 }, max: 40 }, // allow only 40 requests in 2 minutes ], }) async hello(ctx) { this.sendOk(ctx, null, ctx.state.__('hello')); }
-
-
middlewares of a Class
@Route.Route({ middlewares: [ // Array of middlewares async (ctx, next) => { console.log('I will be call before all route in this class'); await next(); }, ], }) class RouteMiddlewares extends Route { async view(ctx, next) { console.log('I will be call after middlewares of class'); this.sendOk(ctx, null, ctx.state.__('hello')); } }
-
middlewares of a specific route
@Route.Get({ middlewares: [ // Array of middlewares async (ctx, next) => { console.log('I will be call before the route but after middlewares of class'); await next(); }, ], }) async view(ctx, next) { console.log('I will be call after middlewares of the class and route'); this.sendOk(ctx, null, ctx.state.__('hello')); }
-
all other fields which aren't in the params object will be rejected
-
simplified writing
params: ['email', 'name'] // is equal to params: { email: false, name: false, } // is equal to params: { email: { __force: false, }, name: false, }
-
more option:
-
__force
[boolean] tells whether a field is required or not -
__func
anArray<Function>
to be executed on the field one by one in order to validate / transform it -
Eg:
params: { name: { __force: false, __func: [ utils.trim, utilsParam.test(utils.notEmpty), // return 400 if empty utils.capitalize, (elem, route, { ctx, body, keyBody }) => { return elem.trim(); }, // do whatever you want... ], }, },
-
-
Eg: object nested inside another object:
params: { user: { __force: true, name: { __force: true, __func: [utils.trim], }, password: true, address: { __force: true, country: true, street: true, } }, date: false, },
Get Started (quick-start boilerplate)
in order to get started quickly, look at this boilerplate, or follow the instructions below:
-
import the app and your middlewares
import { join } from 'path'; // import the app import { App } from 'koa-smart'; // import middlewares koa-smart give you OR others import { bodyParser, compress, cors, handleError, RateLimit, ... } from 'koa-smart/middlewares';
-
create an app listening on port 3000
const myApp = new App({ port: 3000, });
-
add your middlewares
myApp.addMiddlewares([ cors({ credentials: true }), helmet(), bodyParser(), handleError(), RateLimit.middleware({ interval: { min: 1 }, max: 100 }), ... ]);
-
add your routes mount a folder with a prefix (all file who extends from
Route
will be added and mounted)myApp.mountFolder(join(__dirname, 'routes'), '/');
-
Start your app
myApp.start();
-
Basic one
import { join } from 'path'; // import the app import { App } from 'koa-smart'; // import middlewares koa-smart give you OR others import { i18n, bodyParser, compress, cors, helmet, addDefaultBody, handleError, logger, RateLimit, } from 'koa-smart/middlewares'; const myApp = new App({ port: 3000, }); myApp.addMiddlewares([ cors({ credentials: true }), helmet(), bodyParser(), i18n(myApp.app, { directory: join(__dirname, 'locales'), locales: ['en', 'fr'], modes: ['query', 'subdomain', 'cookie', 'header', 'tld'], }), handleError(), logger(), addDefaultBody(), compress({}), RateLimit.middleware({ interval: { min: 1 }, max: 100 }), ]); // mount a folder with an prefix (all file who extends from `Route` will be add and mount) myApp.mountFolder(join(__dirname, 'routes'), '/'); // start the app myApp.start();
-
Other example who Extends class App
import { join } from 'path'; // import the app import { App } from 'koa-smart'; // import middlewares koa-smart give you OR others import { i18n, bodyParser, compress, cors, helmet, addDefaultBody, handleError, logger, RateLimit, } from 'koa-smart/middlewares'; // create an class who extends from App class export default class MyApp extends App { constructor() { super({ port: 3000 }); } async start() { // add your Middlewares super.addMiddlewares([ cors({ credentials: true }), helmet(), bodyParser(), i18n(this.app, { directory: join(__dirname, 'locales'), locales: ['en', 'fr'], modes: ['query', 'subdomain', 'cookie', 'header', 'tld'], }), handleError(), logger(), addDefaultBody(), compress({}), RateLimit.middleware({ interval: { min: 1 }, max: 100 }), ]); // mount a folder with an prefix (all file who extends from `Route` will be add and mount) super.mountFolder(join(__dirname, 'routes')); return super.start(); } } // start the app const myApp = new MyApp(); myApp.start();
you can add you own treatment and attribute to the route.
In this example we will see how you can manage accesses to your route in 2 steps
- Extends
Route
Class and overloadbeforeRoute
methode
export default class MyRoute extends Route {
static accesses = {
public: -1,
connected: 100,
admin: GROUPS.ADMIN_ID,
client: GROUPS.CLIENT_ID,
// whatever ...
};
// overload beforeRoute
async beforeRoute(ctx, infos, next) {
// infos.options content all the param give to the route
if (this.mlCanAccessRoute(ctx, infos.options)) { // test if you can access
this.throw(StatusCode.forbidden, ctx.state.__('Forbidden access'));
}
// call the super methode
await super.beforeRoute(ctx, infos, next);
}
mlCanAccessRoute(ctx, { accesses }) {
if (accesses && Array.isArray(accesses)) {
const { user } = ctx.state;
return accesses.includes(Route.accesses.public) ||
(!!user && (
accesses.includes(Route.accesses.connected) ||
user.usergroup_id === Route.accesses.admin ||
accesses.includes(user.usergroup_id)
));
}
return false;
}
}
- Create a route with access
export default class RouteMyApi extends MyRoute {
constructor(params) {
super({ ...params });
}
@Route.Get({ accesses: [MyRoute.accesses.public] })
async publicRoute(ctx) {
this.sendOk(ctx, ctx.i18n.__('I can be call by any one'));
}
@Route.Get({ accesses: [MyRoute.accesses.client] })
async clientRoute(ctx) {
this.sendOk(ctx, ctx.i18n.__('I can be call by only client user'));
}
@Route.Get({ accesses: [MyRoute.accesses.admin] })
async adminRoute(ctx) {
this.sendOk(ctx, ctx.state.__('I can be call by only admin user'));
}
@Route.Get({ accesses: [MyRoute.accesses.client, MyRoute.accesses.admin] })
async adminRoute(ctx) {
this.sendOk(ctx, ctx.state.__('I can be call by client and admin user'));
}
@Route.Get({ accesses: [MyRoute.accesses.connected] })
async adminRoute(ctx) {
this.sendOk(ctx, ctx.state.__('I can be call by all connected users'));
}
}
MIT © YSO Corp