Skip to content

A framework base on Koajs2 with Decorator, Params checker and a base of modules (cors, bodyparser, compress, I18n, etc…) to let you develop smart api easily

Notifications You must be signed in to change notification settings

ysocorp/koa-smart

Repository files navigation

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, 'User not found');
      this.sendOk(ctx, user);
    }

    // post route: http://localhost:3000/users/add
    @Route.Post({
      accesses: [Route.accesses.public],
      bodyType: Types.object().keys({
        email: Types.string().required(), // return an 400 if the body doesn't contain email key
        name: Types.string().uppercase(), // optional parameter
      }),
    })
    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: **

Install

npm install --save koa-smart Or use the boilerplate (koa-smart-boilerplate)

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, '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, 'hello');
          }
      
      }
  • Get route http://localhost:3000/my-api/hello

      @Route.Get({})
      async hello(ctx) {
        this.sendOk(ctx, null, 'hello');
      }
  • Change path http://localhost:3000/my-api/myroute/15

      @Route.Get({
        path: '/myroute/:id'
      })
      async hello(ctx) {
        this.sendOk(ctx, 'hello' + ctx.params.id);
      }
  • Post route http://localhost:3000/my-api/user-post

      @Route.Post({
        bodyType: Types.object().keys({ // body to allow: all other params will be rejected
          email: Types.string().required(), // return an 400 if the body doesn't contain email key
          name: Types.string().uppercase(), // 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, 'hello');
      }
  • Grant accesses

    Koa smart allows grant permission to be handled in a simple and efficient manner.

    Each function passed to accessers will be given the koa context (ctx) as a parameter, and must return a boolean to express whether is grants access to the route or not.

    If at least one of the function given returns true, access to the route will be granted.

      async function isConnected(ctx) {
        // TODO test if the user is connected
        return ctx.state.user;
      }
      async function isUserPremium(ctx) {
        // TODO test if the user is premium
        return ctx.state.user.isPremium;
      }
      async function isAdmin(ctx) {
        // TODO test if the user is a admin
        return ctx.state.user.isAdmin;
      }
    • Of a Class

      @Route.Route({ accesses: [isConnected] })
      class RouteMiddlewares extends Route {
        @Route.Get({})
        async view(ctx, next) {
          console.log('I can be call if the current client is connected');
          this.sendOk(ctx, null, 'OK');
        }
      }
    • Of a specific route

      @Route.Get({})
      async myPublicRoute(ctx, next) {
        console.log('I am a public route, I can be call by any one');
        this.sendOk(ctx, null, 'OK');
      }
      
      @Route.Get({ accesses: [isConnected] })
      async myConnectedRoute(ctx, next) {
        console.log('I can be call if the current client is connected');
        this.sendOk(ctx, null, 'OK');
      }
      
      @Route.Get({ accesses: [isUserPremium, isAdmin] })
      async myPremiumRoute(ctx, next) {
        console.log('I can be call if the current client is connected and premium or admin');
        this.sendOk(ctx, null, 'OK');
      }
  • 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, '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, '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 {
          @Route.Get({})
          async view(ctx, next) {
            console.log('I will be call after middlewares of class');
            this.sendOk(ctx, null, 'hello');
          }
      }
    • 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, 'hello');
      }

Params checker: See the doc of Types for more information

  • bodyType to check body params

    • quick example

        @Route.Post({ // or Put, Patch
          bodyType: Types.object().keys({
            email: Types.string().regex(/\S+@\S+\.\S+/).required(),
            password: Types.string().min(8).required(),
            address: Types.object().keys({
              country: Types.string().required(),
              street: Types.string().required(),
            }).required(),
          }),
        })
        async user(ctx) {
          // this is the body manage by bodyType
          const bodyParams = this.body(ctx);
      
          // this is the origin body pass
          const originBodyParams = this.body(ctx, true);
        }
  • queryType to check query params

    • quick example

        @Route.Get({
          queryType: Types.object().keys({
            limit: Types.number().integer().required().default(10),
            offset: Types.number().integer().required().default(10),
          }),
        })
        async users(ctx) {
          // this can contain only limit and offset
          const queryParams = this.queryParam(ctx);
      
          // this is the origin queryParams pass
          const originQueryParams = this.queryParam(ctx, true);
        }

Automatic documention generation: See the manual for more information

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({ multipart: true }),
      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({ multipart: true }),
      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({ multipart: true }),
          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();

Upgrade to 4.0.0

4.0.0 upgrades koa2-ratelimit to version 1.0.0

This means koa2-ratelimit does not install sequelize, mongoose or redis anymore. If you use these packages, make sure you install them in your project.

License

MIT © YSO Corp