Nest: [discussion] API Versioning

Created on 10 Jul 2020  ·  6Comments  ·  Source: nestjs/nest

Feature Request

Is your feature request related to a problem? Please describe.

I have an issue when I need to support multiple versions of an API and its routes while running within the same service. Currently it could be accomplished with a prefix on a controller that would end up in the route URI, but I have a requirement where it needs to be done via "Accept Header" versioning.

Describe the solution you'd like

The solution I'm envisioning would:

  • Enable consumers to use the 3 main types of versioning:

    1. URI

    2. Custom Header

    3. Accept Header

  • Allow users to version at a controller level.

    • They would specify in the Controller Decorator options what version(s) the controller is applied for. (See example in comment below)

  • Require consumers to configure which type their application would use (if at all), as well as come potential additional configuration based on the versioning type.

    • Note: The application would only be able to use 1 type of versioning at a time

  • Have the same consumer usage between the different versioning types

Teachability, Documentation, Adoption, Migration Strategy

Users would need to do 2 things to enable versioning:

  1. Configure the versioning type their application will use (See Open question below on where this configuration would happen)
  2. Update controller(s) to include the version configuration

After that, they would be able to create versioned controllers by developing new controllers that are configured for the version they want to make it available for.

What is the motivation / use case for changing the behavior?

I know this has been mentioned in several other issues (https://github.com/nestjs/nest/issues/1268, https://github.com/nestjs/nest/issues/2654), but those mainly focused on URI versioning, which consumers can already use. However, my company has a specific requirement of being able to version a (REST) API via Request headers (i.e. Accept: application/json;v=1). While I know that URI versioning can be handled on the consumer end fairly easily, the others cannot.

I've gotten a POC created in a fork, it currently only supports Accept header versioning.

I will add a comment with the technical design i've created for implementation of the entire feature. I do have some open questions on what consumption would look like that I wanted to get people's thoughts on. Also, I'm more than happy to contribute this feature if the design is approved!

needs triage type

Most helpful comment

NestJS Versioning Design

Goal:
Support 3 types of versioning

  • URI
  • Custom Header
  • Accept Header

High Level Flow (Scenarios):

  1. API is versioned, There are a v1 and v2 of GET /. 
 - Request is made to v1. Request gets routed to v1.
 - Request is made to v2. Request gets routed to v2.
 - Request is made to v3. 404 Response.

Open Questions for edge cases:

  1. If the API is versioned, and we have an un-versioned controller, what would happen?

    1. Would the request automatically go through no matter the version that was sent? (Note, this would be weird for URI versioning)

    2. Would the request be 404’d unless an appropriate version was sent

Adding Versioning

Versioning will be added at a Controller level, using the decorator options. This will set a metadata value “version” with whatever the consumer configured. This will be used w/in the Router when resolving.

Open Question: What should the key be called: version, versions, or something else?

Ex: One Version

@Controller({
  version: '2'
})

Ex: Two Versions

@Controller({
  version: [‘1, ’2’]
})

In addition to the Decorator config, the consumer would need to specify the versioning type, with potential extra config.
Versioning types:

  • URI
  • Custom Header (need to know the header name)
  • Accept Header (need to know the key)

Open Question: Where should the versioning type config be
1. App Module Decorator config?
2. Nest Factory Config?

Route Resolving

For all versioning types, there will be changes made to RouteResolver and RouteExplorer that will detect the versioned routes and process the accordingly.

The processing would be done by statefully grouping routes between the controllers, so that similar routes in different controllers can be referenced in the same function.

URI Versioning

  • Controllers will specify the version, and during the route resolving, the version will be added to the route path, between the prefix and the route’s path (i.e. this/is/a/prefix/v2/route)
  • Requires no specific configuration

Custom Header/Accept Header

  • Controllers will specify the version, and during the route resolving, there will be a wrapper function/filter that will inspect the header and send to the correct target handler depending on version, or respond with a 404 if that route does not exist in that version.
  • Custom Header: Requires configuration for custom header name
  • Accept header: Requires configuration for the version key within the header. (i.e. Accept: application/json;v=1 => key is “v”)

All 6 comments

NestJS Versioning Design

Goal:
Support 3 types of versioning

  • URI
  • Custom Header
  • Accept Header

High Level Flow (Scenarios):

  1. API is versioned, There are a v1 and v2 of GET /. 
 - Request is made to v1. Request gets routed to v1.
 - Request is made to v2. Request gets routed to v2.
 - Request is made to v3. 404 Response.

Open Questions for edge cases:

  1. If the API is versioned, and we have an un-versioned controller, what would happen?

    1. Would the request automatically go through no matter the version that was sent? (Note, this would be weird for URI versioning)

    2. Would the request be 404’d unless an appropriate version was sent

Adding Versioning

Versioning will be added at a Controller level, using the decorator options. This will set a metadata value “version” with whatever the consumer configured. This will be used w/in the Router when resolving.

Open Question: What should the key be called: version, versions, or something else?

Ex: One Version

@Controller({
  version: '2'
})

Ex: Two Versions

@Controller({
  version: [‘1, ’2’]
})

In addition to the Decorator config, the consumer would need to specify the versioning type, with potential extra config.
Versioning types:

  • URI
  • Custom Header (need to know the header name)
  • Accept Header (need to know the key)

Open Question: Where should the versioning type config be
1. App Module Decorator config?
2. Nest Factory Config?

Route Resolving

For all versioning types, there will be changes made to RouteResolver and RouteExplorer that will detect the versioned routes and process the accordingly.

The processing would be done by statefully grouping routes between the controllers, so that similar routes in different controllers can be referenced in the same function.

URI Versioning

  • Controllers will specify the version, and during the route resolving, the version will be added to the route path, between the prefix and the route’s path (i.e. this/is/a/prefix/v2/route)
  • Requires no specific configuration

Custom Header/Accept Header

  • Controllers will specify the version, and during the route resolving, there will be a wrapper function/filter that will inspect the header and send to the correct target handler depending on version, or respond with a 404 if that route does not exist in that version.
  • Custom Header: Requires configuration for custom header name
  • Accept header: Requires configuration for the version key within the header. (i.e. Accept: application/json;v=1 => key is “v”)

Versioning will be added at a Controller level, using the decorator options. This will set a metadata value “version” with whatever the consumer configured. This will be used w/in the Router when resolving.
Open Question: What should the key be called: version, versions, or something else?

What's the difference between this new, proposed property vs explicitly adding this prefix to the path in the @Controller()?

Versioning will be added at a Controller level, using the decorator options. This will set a metadata value “version” with whatever the consumer configured. This will be used w/in the Router when resolving.
Open Question: What should the key be called: version, versions, or something else?

What's the difference between this new, proposed property vs explicitly adding this prefix to the path in the @Controller()?

@kamilmysliwiec For URI versioning, it would be functionally the same As setting the prefix. My thought process would be to offer both for consistency with their versioning feature.

Header based versioning is very important feature IMO.
It is much more convenient for the clients and easier to maintain. We may want to just bump version of the one endpoint to v1.1 so there is no point to create whole prefixed controller etc.
When using headers we could just do similar stuff like Net Core do, so just add the ApiVersion decorator on the Get1_1 so the resolver based on the provided header would go to one of the implementations.
I generally think that API versioning could be heavily benefit from the Net Core MVC Versioning which is just great.

You may take a look at this or similar articles about it:
https://dev.to/99darshan/restful-web-api-versioning-with-asp-net-core-1e8g

any update about this?

I looked at the link that @murbanowicz shared and really liked some of the patterns that .NET uses. I've incorporated some of the patterns in that doc and have made some small revisions to the design, see below for the revised design.

As far as contribution goes, as long as @kamilmysliwiec (or anyone else that's interested) has no objections, I'm good to contribute this feature. I'm working with my company's open source office so I can contribute it through them, otherwise it's going to take longer. Given that the Holidays coming up, I'm hoping to have everything finished by early January.

NestJS Versioning Design (Revised)


Click to expand!

Goal

Support 3 types of versioning:

  • URI
  • Header
  • Media Type (Accept Header):

URI Versioning Overview

  • Controllers will specify the version, and during the route resolving, the version will be added to the route path, between the prefix and the route’s path (i.e. this/is/a/prefix/v2/route)
  • Requires no specific configuration

Note: URI versioning can already be accomplished with the controller prefix option. It would be included within this feature for purely a consistency pattern. Either the controller prefix, or version pattern could be used.

Header/Media Type Versioning Overview

  • Controllers will specify the version, and during the route resolving, there will be a wrapper function/filter that will inspect the header and send to the correct target handler depending on version, or respond with a 404 if that route does not exist in that version.
  • Custom Header: Requires configuration for custom header name
  • Accept Header: Requires configuration for the version key within the header. (i.e. Accept: application/json;v=1 => key is v)

Adding Versioning to an Application

Controller Level Decorator

Versioning will be added at a Controller level, using the decorator options. This will set a metadata value version with whatever the consumer configured. This will be used w/in the Router when resolving.

There will also be an option to specify a controller as "Version Neutral" which indicates that it does not depend on the version (i.e. a Health Check Endpoint). A controller tagged with Version Neutral will work for any version passed, or if no version is passed.

Ex: One Version

@Controller({
  version: '2'
})

Ex: Multiple Versions

@Controller({
  version: ['1', '2']
})

Ex. Version Neutral

@Controller({
  version: VERSION_NEUTRAL // VERSION_NEUTRAL will be an exported const (Symbol?)
})

Method Level Decorator

For specific handlers within a controller, a new decorator (@Version) will be created allow that one handler to go to a specific version.

This will override whatever version is set at the controller level.

It will allow for finer granularity for versioning and reduce potential code duplication

Ex.

@Controller({
  version: '1'
})
class TestController {
  ...
  @Get('/test')
  test_v1() {
    ...
  }

  @Version('2')
  @Get('/test')
  test_v2() {
    ...
  }
}

Nest Application Method

In addition to the Decorator config, the consumer would need to specify the versioning type with their corresponding required config.

The consumer would specify this in their main.ts (or equivalent) file using a new class method on the NestApplication class.

The new method would be called enableVersioning and would take an options object that would have several properties:

  • type: Specifies the type of the versioning (uri, header, media type).

    • Would use an ENUM for the type and corresponding Interfaces for the with the required configuration

  • defaultVersion: Optional prop, if present, it would allow incoming requests that don't have the version specified to get mapped to a default version.

These options would be extended in the future to allow for more features. An example of a future features could be Reporting API Versions (I don't plan on including that in the initial contribution).

Ex: URI

const app = await NestFactory.create(AppModule);

app.enableVersioning({
  type: VERSION.URI
})

Ex: Header

const app = await NestFactory.create(AppModule);

app.enableVersioning({
  type: VERSION.HEADER
  header: 'X-version'
})

Ex: Media Type (Accept)

const app = await NestFactory.create(AppModule);

app.enableVersioning({
  type: VERSION.MEDIA_TYPE
  key: 'v'
})

Route Resolving

For all versioning types, there will be changes made to RouteResolver and RouteExplorer that will detect the versioned routes and process the accordingly.

The processing would be done by statefully grouping routes between the controllers, so that similar routes in different controllers can be referenced in the same function, which will use the versioning configuration to determine which route the request will be routed to.

High Level Flow (Scenarios):

  1. API is versioned, There are a v1 and v2 of GET /.

    • Request is made to v1. Request gets routed to v1.

    • Request is made to v2. Request gets routed to v2.

    • Request is made to v3. 404 Response.

  2. API is versioned. Contains an unversioned controller for GET /test

    • Request is made to /test for any version. 404 Response.

  3. API is versioned. Contains a "Version Neutral" controller for GET /neutral

    • Request is made to /neutral for any version. Response from controller.

    • Request is made to /neutral that does not specify version. Response from controller.

  4. API is versioned. There are a v1 and v2 of GET /. v1 is specified as the default

    • Request is made to v1. Request gets routed to v1.

    • Request is made to v2. Request gets routed to v2.

    • Request is made to v3. 404 Response.

    • Request is made w/out a specified version. Request gets routed to v1.

High Level Changes in required NestJS

  1. Adding new configuration to the @Controller decorator
  2. Add new @Version decorator
  3. Add new method enableVersioning to the NestApplication class
  4. Update RouteExplorer and RouteResolver to facilitate the version matching logic
  5. Docs!
Was this page helpful?
0 / 5 - 0 ratings

Related issues

anyx picture anyx  ·  3Comments

2233322 picture 2233322  ·  3Comments

artaommahe picture artaommahe  ·  3Comments

breitsmiley picture breitsmiley  ·  3Comments

mishelashala picture mishelashala  ·  3Comments