npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

express-versioning

v1.3.0

Published

Express middleware for resolving controllers with api versioning support.

Downloads

7

Readme

Build Status codecov NPM Dependency Status

express-versioning

Express middleware for resolving controllers with api versioning support.

Installation

npm install express-versioning --save

Getting Started

  1. Create file structure for controllers
 - controllers/
    - v1/
        - users.js
        - posts.js
        - comments.js
    - v2/
        - users.js
        ...

The name of files should have the same name as the resource, which will appear in the path of the route. So that users.js => /users. If you want to use any prefix or suffix for your file names see controllerPattern

  1. Setup controllers

    1. Object literal approach
    // v1/users.js
        
    const users = {
           
       getUser(req, res, next) { /* ... */ },
       
       getUsers(req, res, next) { /* ... */ }
    };
        
    export default users;
       
    // v2/users.js
    import v1User from '../v1/users.js'
       
    const users = Object.create(v1User); // link usersV2 to usersV1 via prototype chain
    // override
    users.getUser = function (req, res, next) { /* ... */ };
       
    export default users;
       

    To make functionality of previous versions available in a newly created version, the newly created version has to be linked to the previous version. This is achieved with Object.create(v1User). If you don't want to make previous functionality available, don't link the controllers

    1. Class approach
    // v1/users.js
        
    export default class UserController {
    
      getUser(req, res, next) { /* ... */ }
        
      getUsers(req, res, next) { /* ... */ }
    }
       
    // v2/users.js
    import V1UserController from '../v1/users.js'
       
    export class UserController extends V1UserController {
     
      // override
      getUser(req, res, next) { /* ... */ }
      
    }
       

    To make functionality of previous versions available in a newly created version, the newly created version has to extend the controller of the previous version. If you don't want to make previous functionality available, don't extend the controllers of the previous version

    If you're using dependency injection, you can set a getter function or an injector (see here)

If you prefer using named exports, make sure, that the filename and the name of the exported controller are the same

  1. Setup middleware and routes
import {controllers} from 'express-versioning';

app.use(controllers({
  path: __dirname + '/controllers'
}));

app.get('/:version/users', (req, res, next) => req.controller.getUsers(req, res, next));
app.get('/:version/users/:id', (req, res, next) => req.controller.getUser(req, res, next));

Abstract controllers

If you want to define abstract controllers for you routes, you can do so by creating an abstract folder on the same level as the version folders:

 - controllers/
    - abstract/
        - users.js
        - posts.js
        - comments.js
    - v1/
        - users.js
        - posts.js
        - comments.js
    - v2/
        - users.js
        ...

The name of the abstract folder can be changed (see here).

Complex file structures

If your controllers are structured much more complex like:

 - controllers/
    - organization-a/
        - v1/
          - users.js
        - v2/
          - users.js
    - organization-b/
        - v1/
          - users.js
          - sub-organization/
            - documents.js
        ...

express-versioning can also handle this for you and will resolve the controllers of the example to the following routes:

organization-a/:version/users
organization-b/:version/users
organization-b/:version/sub-organiuation/documents

Defining routes directly in controllers with TypeScript

With TypeScript you're able to define the routes of its corresponding route handlers directly in the controller class of these handlers. Therefore annotations come into play.

Configuration

To use this feature, you need to install reflect-metadata and need to set some flags in your tsconfig.json:

reflect-metadata

npm install reflect-metadata --save

tsconfig.json

"experimentalDecorators": true,
"emitDecoratorMetadata": true

Usage

To define get routes, annotate the appropriate route handlers with a @Get annotation. The same works for all http methods, that are supported by express. Please notice, that you should not use the resource name in the path, since it is already set due to the filename.

// /v1/UserController.js
import {Request, Response, NextFunction} from 'express';
import {Get, Post, Put} from 'express-versioning';

export class UserController {

  @Get('/:id')
  getUser(req: Request, res: Response, next: NextFunction) { /* ... */ }

  @Get
  getUsers(req: Request, res: Response, next: NextFunction) { /* ... */ }

  @Get('/:id/posts')
  getUserPosts(req: Request, res: Response, next: NextFunction) { /* ... */ }

  @Post
  postUser(req: Request, res: Response, next: NextFunction) { /* ... */ }

  @Put('/:id')
  putUser(req: Request, res: Response, next: NextFunction) { /* ... */ }
}

Configure middleware

resolveRouteHandler need to be set to true.

import {controllers} from 'express-versioning';

app.use(controllers({
  path: __dirname + '/controllers',
  resolveRouteHandler: true,
  controllerPattern: /(.*?)Controller/
}));

Overriding route handlers

When overriding route handlers of previous versions, you must not define the route for its handler again. But must instead use the @OverrideRouteHandler annotation. Otherwise express-versioning throws an error. This will ensures, that route handlers will not be overridden by accident. Furthermore, it makes clear, that the @OverrideRouteHandler annotated function is a route handler.

// /v2/UserController.js
import {OverrideRouteHandler} from 'express-versioning';
import {UserController as V1UserController} from '../v1/UserController';

export class UserController extends V1UserController {

  @OverrideRouteHandler
  getUser(req: Request, res: Response, next: NextFunction) { /* ... */ }
}

Set resource name explicitly

If you don't want the resource name to be inferred by file name automatically, you can do so, by setting the resource name explicitly with @Resource:

import {Resource} from 'express-versioning';

@Resource('users')
export class UserController {

}

or with starting "/"

import {Resource} from 'express-versioning';

@Resource('/users')
export class UserController {

}

API

controllers options

/**
 * Path to controllers
 */
path: string;

/**
 * Regex pattern to recognize a version folder
 * @default /^(v\d.*)$/
 */
versionPattern?: RegExp;

/**
 * Regex pattern to recognize controller files
 * @default /^(.*?)$/
 */
controllerPattern?: RegExp;

/**
 * Name of directory in which abstract controllers can be
 * found
 * @default abstract
 */
abstractDir?: string;

/**
 * Prints some info to console if true. Default is false
 * @default false
 */
debug?: boolean;

/**
 * Indicates if routes handlers should be resolved from
 * controllers automatically or not
 * @default false
 */
resolveRouteHandler?: boolean;

/**
 * Injector to inject controller class instance
 */
injector?: {get<T>(model: any): T};

/**
 * Inject function to inject a controller class instance
 */
inject?<T>(model: any): T;

Annotations


/**
 * Stores http method and path as metadata for target prototype
 */

@Get
@Get(path: PathParams)

@Put
@Put(path: PathParams)

@Post
@Post(path: PathParams)

// ... works for all http methods, that are supported by express

Why

When creating a REST api with version support, there will be some problems you will face:

The definition of the same routes for all versions again and again and again ... and the need to care about which controller should be used for which version:

app.get('/v1/users', (req, res, next) => usersController.getUsers(req, res, next));
app.get('/v2/users', (req, res, next) => usersController1.getUsers(req, res, next));
app.get('/v3/users', (req, res, next) => usersController2.getUsers(req, res, next));
/* ... */

The definition of routes of previous versions for next versions. When creating a next version, not all routes should be created again for a new version. Especially when not all routes have changed compared to the previous version:

// version 1
app.get('/v1/users', (req, res, next) => usersController.getUsers(req, res, next));
app.get('/v1/users/:id', (req, res, next) => usersController.getUser(req, res, next));
app.post('/v1/users', (req, res, next) => usersController.postUser(req, res, next));

// version 2
app.get('/v2/users', (req, res, next) => usersController2.getUsers(req, res, next));

// no changes here (see "userController" instead of "userController2")
app.get('/v2/users/:id', (req, res, next) => usersController.getUser(req, res, next));

app.post('/v2/users', (req, res, next) => usersController2.postUser(req, res, next));

Route /v2/users/:id has to be defined again, despite of nothing has changed in version 2 in getUser implementation. But to make this endpoint also available in version 2, we had to do so.

Since DRY is not satisfied, all these issues will probably result in bugs

express-versioning solves these problems for you. So that you don't need to repeat yourself.