Question
· Apr 21, 2023

REST API Versioning in Header

I have seen this post and appreciate that discussion: API RESTful Version | InterSystems Developer Community | Business Service

However our org requirement is for a caller to provide the API version in the HTTP request header.  I am finding the cleanest way to route to the correct class using the header version.  My classes are setup as

API.Service.v1

API.Service.v2

Of course only v1 exists now but when v2 goes live on future day, API.Service.v2 will extend API.Service.v1 so we only have to override the method that changes or add a new method if needed.  

I thought I might be able to modify %reqeust.URL before the dispatch method takes over.  OnPreDispatch takes pUrl by value and it's already set from %request.URL way ahead of that method.  Same with AccessCheck().  I tried OnPreHTTP() as well but that method doesn't even seem to be firing.  

My understanding is that passing the version in the path is just as common as passing in the header, so I'm wondering if someone has solved this.

Other ideas swimming in my head:

- Just route to one class and do logical switches in each method based on the version in the header (this might get messy if we do a ton of versions but might be fine if we don't have a ton of versions).

- I thought I could do a "preroute" and modify %request.URL before it went to the proper router, but testing that out, I think I'm nested in the stack somewhere where I can't figure and it's not working as I want it to.

Any ideas?

Product version: IRIS 2021.1
$ZV: IRIS for Windows (x86-64) 2022.1.2 (Build 574U) Fri Jan 13 2023 15:00:26 EST
Discussion (2)2
Log in or sign up to continue

Hi @Michael Davidovich - it's been a while! Here's a quick sample for how I'd do this:

Class Mike.Demo.REST Extends %CSP.REST
{

/// This method gets called prior to dispatch of the request. Put any common code here
/// that you want to be executed for EVERY request. If pContinue is set to 0, the
/// request will NOT be dispatched according to the UrlMap. In this case it's the
/// responsibility of the user to return a response.
ClassMethod OnPreDispatch(pUrl As %String, pMethod As %String, ByRef pContinue As %Boolean) As %Status
{
    #dim %request As %CSP.Request
    Set pContinue = 0
    Set version = %request.GetCgiEnv("HTTP_X_API_VERSION","unspecified; use X-API-VERSION header")
    Set class = $Case(+version,
        1:"Mike.Demo.v1",
        2:"Mike.Demo.v2",
        :"")
    If (class = "") {
        Set error = $$$ERROR($$$GeneralError,$$$FormatText("Invalid API version: %1",version))
        // Shoud be HTTP 400, but you probably want to report this differently/better.
        Do ..ReportHttpStatusCode(..#HTTP400BADREQUEST,error)
        Quit $$$OK
    }
    
    Quit $classmethod(class,"DispatchRequest",pUrl,pMethod,1)
}

}

Class Mike.Demo.v1 Extends %CSP.REST
{

Parameter VERSION = 1;

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<Route Url="/version" Method="GET" Call="GetVersion" />
</Routes>
}

ClassMethod GetVersion() As %Status
{
    Write {"version":(..#VERSION)}.%ToJSON()
    Quit $$$OK
}

}

Class Mike.Demo.v2 Extends Mike.Demo.v1
{

Parameter VERSION = 2;

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<Route Url="/version" Method="GET" Call="GetVersion" />
</Routes>
}

}

Thanks, @Timothy Leavitt!

This was incredibly helpful.  I modified it a bit as that $CASE statement would have grown over time into something not fun to manage.

I created the main dispatch class and prepended the version to the pUrl parameter in the OnPredispatch method and then passed that into a main router class using DispatchRequest similar do what you did there.  We can maintain the routemap in that main router class to keep things more organized and standard and from there it simply acts as normal.

THANK YOU!