Skip to content

Latest commit

 

History

History
560 lines (465 loc) · 15.3 KB

README-V1.md

File metadata and controls

560 lines (465 loc) · 15.3 KB

KoaSmart is a framework based on Koajs2, which allows you to develop RESTful APIs with : Class, Decorator, Params checker

Build Status NPM version

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);
    }

  }

Summary

What is in this framework ?

**This framework gives you the tools to use a set of modules: **

the full documentation for this module can be found here

Install

npm install koa-smart

Router with decorator

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 ?

      1. the Route word is removed
      2. 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'));
          }
      
      }
    • 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'));
    }

Params checker of POST body

  • 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 an Array<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,
    },

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();

Full example

  • 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();

Add treatment on route

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

  1. Extends Route Class and overload beforeRoute 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;
  }
}
  1. 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'));
  }
}

License

MIT © YSO Corp