typestack / routing-controllers

Create structured, declarative and beautifully organized class-based controllers with heavy decorators usage in Express / Koa using TypeScript and Routing Controllers Framework.
MIT License
4.42k stars 394 forks source link

Feature request for API Versioning #299

Closed StickNitro closed 1 year ago

StickNitro commented 7 years ago

Would like to request adding support for API versions, similar to ASP.Net Core where you could specify the default version in configuration and then using a decorator on a controller or controller method specify which version it supports, including being able to map say two or more different versions of a get method to the same get.

NoNameProvided commented 7 years ago

I like the idea of inmplementing this in routing-controllers. What way would you want to send the version information?

MichalLytek commented 7 years ago

Would like to request adding support for API versions, similar to ASP.Net Core

Some of us doesn't know ASP.Net or Spring. Could you describe your proposal with more details? How the API should look and how it should behave? 😉

StickNitro commented 7 years ago

You can implement versioning in Express at the moment by adding two different routes that will use two different callback methods to service the request and using URL path based version, query string versioning or HTTP header base versioning. I believe that all three could be supported and the appropriate configuration and decorators provided to configure routing-controllers

I would propose that the routing-controllers configuration support the ability to enable versions and allow services to specify say a default version along with other metadata.

    useExpressServer(app, {
        routePrefix: "/api",
        controllers: [
            __dirname + "/controllers/**/*{.js,.ts}"
        ],
        **apiVersioning: {
            enabled: true,
            assumeDefaultVersionWhenUnspecified: true|false,
            defaultApiVersion: "1.0",
            reportApiVersions: true|false,
            supportedVersions: ["~1.0", "^2.0"]
        }**
    });

I would then propose the addition of a decorator that could be added to either a class or an individual method, the decorator could be called ApiVersion

Applied for the whole controller (based on the consumer providing the version in the HTTP Header)

/*
HTTP Head
X-API-Version: 1.1
*/

@ApiVersion("1.0")
@ApiVersion("1.1")
@JsonController("/hello")
export class HelloWorldController {
}

But this could also be achieved using the URL path method

/*
http://localhost/api/hello/1.0/world/
*/
@ApiVersion("1.0")
@JsonController("/hello/:apiVersion/world")
export class HelloWorldv1Controller {
}

/*
http://localhost/api/hello/2.0/world/
*/
@ApiVersion("2.0")
@JsonController("/hello/:apiVersion")
export class HelloWorldv2Controller {
}

Applied at the controller method level

@ApiVersion("~1.0") // could even support NPM version styles as well
@JsonController("/hello")
export class HelloWorldController {
    @ApiVersion("1.0")
    @Get("/:id")
    async readOne10(...) {
        return "Hello from version 1.0";
    }

    @ApiVersion("1.1")
    @Get("/:id")
    async readOne11(...) {
        return "Hello from version 1.1";
    }
}

There could also be a decorator to allow versioning of a single method

@ApiVersion("1.0")
@ApiVersion("1.1", {deprecated: true})
@ApiVersion("2.0")
@JsonController("/hello")
export class HelloWorldController {
    @Get("/:id")
    async readOne(...) {
        return "Hello World (v1)";
    }

    **@MapToApiVersion("2.0")**
    @Get("/:id")
    async readOnev2(...) {
        return "Hello World (v2)";
    }
}

Routing controllers would then register the routes in express, consumers would opt-in to a version by specifying a HTTP header in the request (or using URL path based, etc.), if assumeDefaultVersionWhenUnspecified is true and no version is specified in the request then routing-controllers would forward the request to the defaultApiVersion route. If an invalid version is specified of no route exists then a 404 can be thrown (this could include a message stating that the version is not supported)

If reportApiVersions is true then a header can be included in the response to return the list of supported versions for that route as defined by the ApiVersion decorator and the MapToApiVersion decorator along with a header of any deprecated versions. The output HTTP headers could be:

Response Headers
    api-supported-versions: 1.0, 1.1, 2.0
    api-deprecated-version: 1.1

Not sure whether this would be relevant but you could also have a decorator similar to the @CurrentUser decorator that will allow a service method to inject the version as a parameter into the controller method, eg. @CurrentVersion() apiVersion: string

Hope that all makes sense :)

Diluka commented 7 years ago

I'm not talking about whether this feature should be or not to be developed.

I don't think it's a good idea to contain multiple versions of one API into one branch. You should use nginx to proxy different version of servers into different path.

Different version of APIs may have different logics and data structures. Put them in the same branch, one day will be a mess.

MichalLytek commented 7 years ago

@StickNitro Ok, I get the idea but I have some doubts about details.

could even support NPM version styles as well

This is semver actually. But I don't find it useful in this case: When you have v1.0 api and introduces some changes in v1.1, it can't modify the existing routes due to no breaking changes. It can only add new routes or deprecate the old in favour of new. If you need to change something in the routes, like some resource has changed query options names or returned entity, it needs to be v2.0 api as breaking changes.

So my question is what are the rules on mapping request to actions. You gave an example:

/*
HTTP Head
X-API-Version: 1.1
*/

@ApiVersion("1.0")
@ApiVersion("1.1")
@JsonController("/hello")
export class HelloWorldController {
}

But 1.1 shouldn't be compatible with 1.0? So all 1.0 routes are available for 1.1 API request? Or we can overwrite the older route (it may be hard to implement and considered bad practice) How 2.0 should work then? Only actions decorated with @ApiVersion("2.0") are available, right?

About details:

There could also be a decorator to allow versioning of a single method

We don't need separate decorator, @ApiVersion can be universal and applied on class and method level.

enabled: true,

Not needed - config object means true, just like validation works now.

supportedVersions: ["~1.0", "^2.0"]

Shouldn't this be auto generated from decorators?

assumeDefaultVersionWhenUnspecified: true|false, defaultApiVersion: "1.0",

It should be just defaultApiVersion option - if not provided, we can assume that assumeDefaultVersionWhenUnspecified is false.

But this could also be achieved using the URL path method

I am against dynamic api version in path, it should be global prefix like we can specify /api. It can check then the path, the query string and header in the end.

Not sure whether this would be relevant but you could also have a decorator similar to the @CurrentUser decorator that will allow a service method to inject the version as a parameter into the controller method, eg. @CurrentVersion() apiVersion: string

Yes, it might help someone and it's not hard to implement. However I'm not sure about universal route with dynamic handling the api version.

MichalLytek commented 7 years ago

I don't think it's a good idea to contain multiple versions of one API into one branch. You should use nginx to proxy different version of servers into different path.

Different version of APIs may have different logics and data structures. Put them in the same branch, one day will be a mess.

So you have your public API and need to change something in one route for your new version of mobile app. And you propose to copy paste the whole code and db and then make changes and map to nginx? Isn't it simpler to have additional route for new api version and the rest of the app would work normally?

StickNitro commented 7 years ago

@19majkel94 I will try to answer all your points as best I can

could even support NPM version styles as well

This was just a thought I had when submitting the request and not essential, the basic principle around the use of Major.Minor was that API consumers could "opt-in" to a version of the API. This would allow the API to develop at a different pace to any connected UI, the UI would then be able to "opt-in" to an available version. The example I provided (in particular the ability to have two methods for the same route but for differing versions (e.g. 1.0 and 1.1)) was following a minor release to the API to add a non-breaking change that a UI could choose to opt-in to when ready (although I appreciate this could also conceivably be classed as a breaking change). The idea being that consumers could always use the latest API version available through the defaultApiVersion but could equally lock the UI to a specific version of the API

There could also be a decorator to allow versioning of a single method

That sounds fine

supportedVersions: ["~1.0", "^2.0"] Shouldn't this be auto generated from decorators?

Yes it could be rather than manually configured, makes more sense if can be automated

assumeDefaultVersionWhenUnspecified: true|false, defaultApiVersion: "1.0", It should be just defaultApiVersion option - if not provided, we can assume that assumeDefaultVersionWhenUnspecified is false.

Makes sense

But this could also be achieved using the URL path method I am against dynamic api version in path, it should be global prefix like we can specify /api. It can check then the path, the query string and header in the end.

I agree that providing the version in the URL path is not a good way to achieve this, again this was an idea at the time of requesting and is primarily based around a consumer opting into a version as opposed to being configured on the API

The premise here and in my comment at the beginning being that both the API and the UI may have different teams working on them and be working at different speeds. The API team would be able to release new features (be they major changes or minor enhancements). The UI team(s) would be able to include features in the UI and opt-in to new API features when ready simply by providing the correct version in the HTTP header

Not sure whether this would be relevant but you could also have a decorator similar to the >>@currentuser decorator that will allow a service method to inject the version as a parameter into the >>controller method, eg. @currentversion() apiVersion: string Yes, it might help someone and it's not hard to implement. However I'm not sure about universal route >with dynamic handling the api version.

I see this as exposing the version provided in the HTTP header, perhaps this is the way (and possibly the best way) of providing which version of an API you are calling.

abinici commented 7 years ago

At my working place we have a Proxy API created with NodeJS & ExpressJS. In the beginning we used api-level versioning, which we quickly found out was not flexible enough.

So I began searching for an alternative solution and found this library which allowed us to version per route:

express-versioning-router

With this library, we could now have two versions of the same route:

router.get(1, '/hello', function(req, res, next) {
    ...
});

router.get(42, '/hello', function(req, res, next) {
    ...
});

This way, our clients (we have 3) could consume the new version of a route at different paces and avoided breaking changes.

Based on what we went through at my working place, I agree with the proposal from @StickNitro.

If this feature request will be implemented I hope you will decide to support versioning at the method level because this will give the optimal level of flexibility.

BenjD90 commented 6 years ago

@19majkel94

Looking in the project code, I think that it might be a lot more easy to add the API version to the "action" decorator. For instance, with @Get :

export function Get(route?: string|RegExp, version?: string = ''): Function {
    return function (object: Object, methodName: string) {
        getMetadataArgsStorage().actions.push({
            type: "get",
            target: object.constructor,
            method: methodName,
            route: route,
            version: version
        });
    };
}

Then deal with this new version attribut to register the action.

Usage :

    @Get("/:id", "1.0")
    async readOne10(...) {
        return "Hello from version 1.0";
    }
    @Get("/:id", "1.1")
    async readOne11(...) {
        return "Hello from version 1.1";
    }

To manage multiple versions for one route, it could be done with a versions array.

georgyfarniev commented 5 years ago

Hello. Would be nice to have this feature. Cast @jotamorais

github-actions[bot] commented 4 years ago

Stale issue message

attilaorosz commented 1 year ago

Closing this as stale.

If the issue still persists, you may open a new Q&A in the discussions tab and someone from the community may be able to help.

github-actions[bot] commented 1 year ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.