Skip to main content

Creating a REST Service Manually

This appendix describes how to manually create an InterSystems IRIS® data platform REST service by subclassing the %CSP.RESTOpens in a new tab class; this procedure creates a manually-coded REST service that does not work with all the API management tools.

Also see Setting Up Authentication for REST Services in Securing REST Services.

Basics of Creating a REST Service Manually

To define a REST service manually, do the following:

  • Create a REST service class — subclass of %CSP.RESTOpens in a new tab. In your subclass:

    • Define a URL map that specifies the InterSystems IRIS method that is executed for a REST URL and HTTP method.

    • Optionally specify the UseSession parameter. This parameter controls whether each REST call is executed under its own web session or shares a single session with other REST calls.

    • Optionally, override error handling methods.

    If you want to separate the implementation code from the dispatch code, you can define the methods implementing the REST services in separate class and invoke those methods from the URL map.

  • Define a web application that uses the REST service class as its dispatch class.

    To define the web application and its security, go to the Web Application page (click System Administration > Security > Applications > Web Applications).

    When you define the web application, you set the Dispatch Class to the name of your REST service class.

    Also, specify the name of the application as the first part of the URL for the REST calls. An example name is /csp/mynamespace or /csp/myapp, but you can specify any text that is allowed in a URL.

You can define more than one REST service class in a namespace. Each REST service class that has its own entry point must have its own web application.

Creating the URL Map

In the REST service class, define an XData block named UrlMap that associates the REST call with the method that implements the service. It can either directly send the call to a method based on the contents of the URL or it can forward the call to another REST service class based on the URL. If the web application is handling a small number of related services, you can send the call directly to the method that implements it. However, if the web application is handling a large number of disparate services, you can define separate REST service classes, each of which handles a set of related services. Then configure the web application to use a central REST service class that forwards the REST calls to other REST service classes as appropriate.

If the subclass of %CSP.RESTOpens in a new tab is sending the call directly to the methods, the URLMap contains a <Routes> definition that contains a series of <Route> elements. Each <Route> element specifies a class method to be called for the specified URL and HTTP operation. Typically REST uses the GET, POST, PUT, or DELETE operations, but you can specify any HTTP operation. The URL can optionally include parameters that are specified as part of the REST URL and passed to the specified method as parameters.

If the subclass of %CSP.RESTOpens in a new tab is forwarding the calls to other subclasses of %CSP.RESTOpens in a new tab, the UrlMap contains a <Routes> definition that contains a series of <Map> elements. The <Map> element forwards all calls with the specified prefix to another REST service class, which will then implement the behavior. It can implement the behavior by sending the call directly to a method or by forwarding it to another subclass.

If a REST service class is a subclass, it can inherit routes from a UrlMap defined in its superclass. If the <Routes> element specifies Extend="true", the routes in the superclass’s UrlMap are merged with those in the current class. You can remove a route defined in a superclass by defining a matching route in the current class with Disabled="true". This capability can be used to better support REST API versioning, by allowing newer versions to contain UrlMaps that express additions to previous versions, while retaining a base set of capabilities.

Important:

InterSystems IRIS compares the incoming REST URL with the URL property of each <Route> and the Prefix property of each <Map>, starting with the first item in the URL map, and stopping at the first possible match that uses the same HTTP request method. Thus the order of the elements in the <Routes> is significant. If an incoming URL could match multiple elements of the URL map, InterSystems IRIS uses the first matching element and ignores any subsequent possible matches.

URLMap with <Route> Elements

InterSystems IRIS compares the incoming URL and the HTTP request method to each <Route> element in the URL map. It calls the method specified in the first matching <Route> element. The <Route> element has three parts:

  • Url — specifies the format of the last part of the REST URL to call the REST service. The Url consists of text elements and parameters prefaced by : (colon).

  • Method — specifies the HTTP request method for the REST call: typically these are GET, POST, PUT, or DELETE, but any HTTP request method can be used. You should choose the request method that is appropriate for the function being performed by the service, but the %CSP.RESTOpens in a new tab class does not perform any special handling of the different method. You should specify the HTTP request method in all uppercase letters.

  • Call — specifies the class method to call to perform the REST service. By default, this class method is defined in your REST service class, but you can explicitly specify any class method.

For example, consider the following <Route>:

<Route Url="/echo" Method="POST" Call="Echo" Cors="false" />

This specifies that the REST call will end with /echo and use the POST method. It will call the Echo class method in the REST.DocServer class that defines the REST service. The Cors property is optional; see Modifying a REST Service to Use CORS for details.

The complete REST URL consists of the following pieces:

  • The base URL of your InterSystems IRIS instance.

  • The name of the web application as defined on the Web Application page (click System Administration > Security > Applications > Web Applications). (For example, /csp/samples/docserver)

  • The Url property of the <Route> element. If a segment of the Url property is preceded by a : (colon), it represents a parameter. A parameter will match any value in that URL segment. This value is passed to the method as a parameter.

For the preceding example, the complete REST call as shown by a TCP tracing utility is:

POST /csp/samples/docserver/echo HTTP/1.1
Host: localhost:52773

If you want to separate the code implementing the REST services from the %CSP.RESTOpens in a new tab dispatch code, you can define the methods implementing the REST services in another class and specify the class and method in the Call element.

Specifying Parameters

The following <Route> definition defines two parameters, namespace and class, in the URL:

<Route Url="/class/:namespace/:classname" Method="GET" Call="GetClass" />

A REST call URL starts with /csp/samples/docserver/class/ and the next two elements of the URL specify the two parameters. The GetClass() method uses these parameters as the namespace and the class name that you are querying. For example, consider this REST call:

http://localhost:52773/csp/samples/docserver/class/samples/Cinema.Review

This REST call invokes the GetClass() method and passes the strings "samples" and "Cinema.Review" as the parameter values. The GetClass() method has the following signature:

/// This method returns the class text for the named class
ClassMethod GetClass(pNamespace As %String,
                     pClassname As %String) As %Status
{

Specifying Multiple Routes for a Single URL

For a given URL, you can support different HTTP request methods. You can do either by defining separate ObjectScript methods for each HTTP request, or by using a single ObjectScript method that examines the request.

The following example uses different methods for each HTTP request method for a single URL:

<Route Url="/request" Method="GET" Call="GetRequest" />
<Route Url="/request" Method="POST" Call="PostRequest" />

With these routes, if the URL /csp/samples/docserver/request is called with an HTTP GET method, the GetRequest() method is invoked. If it is called with an HTTP POST method, the PostRequest() method is invoked.

In contrast, you could use the following <Route> definitions:

<Route Url="/request" Method="GET" Call="Request" />
<Route Url="/request" Method="POST" Call="Request" />

In this case, the Request() method handles the call for either a GET or a POST operation. The method examines the %request object, which is an instance of %CSP.RequestOpens in a new tab. In this object, the URL property contains the text of the URL.

Regular Expressions in the Route Map

You can use regular expressions within the route map. InterSystems suggests that you do so only if there is no other way to define the REST service to meet your needs. This section provides the details. (For information on regular expressions in ObjectScript, see Regular Expressions.)

Internally, the :parameter-name syntax for defining parameters in the URL is implemented using regular expressions. Each segment specified as :parameter-name is converted to a regular expression that contains a repeating matching group, specifically to the ([^/]+) regular expression. This syntax matches any string (of non-zero length), as long as that string does not include the / (slash) character. So the GetClass() sample, which is Url="/class/:namespace/:classname", is equivalent to:

<Route Url="/class/([^/]+)/([^/]+)" Method="GET" Call="GetClass" />

where there are two matching groups that specify two parameters.

In most cases this format provides enough flexibility to specify the REST URL, but advanced users can use the regular expression format directly in the route definition. The URL must match the regular expression, and each matching group, which is specified by a pair of parentheses, defines a parameter to be passed to the method.

For example, consider the following route map:

<Routes>
<Route Url="/Move/:direction" Method="GET" Call="Move" />
<Route Url="/Move2/(east|west|north|south)" Method="GET" Call="Move" />
</Routes>

For the first route, the parameter can have any value. No matter what value the parameter has, the Move() method is called. For the second route, the parameter must be one of east west north or south; if you call the second route with a parameter value other than those, the Move() method is not called, and REST service returns a 404 error because the resource cannot be found.

This simple example is meant only to demonstrate the difference between the usual parameter syntax and a regular expression. In the case discussed here, there is no need for a regular expression because the Move() method can (and should) check the value of the parameter and respond appropriately. In the following cases, however, a regular expression is helpful:

  • If a parameter is optional. In this case, use the regular expression ([^/]*) instead of the :parameter-name syntax. For example:

    <Route Url="/Test3/([^/]*)" Method="GET" Call="Test"/>
    

    Of course, the method being called must also be able to handle having a null value for the parameter.

  • If the parameter is the last parameter and its value can include a slash. In this case, if the parameter is required, use the regular expression ((?s).+) instead of the :parameter-name syntax. For example:

    <Route Url="/Test4/((?s).+)" Method="GET" Call="Test"/>
    

    Or, if this parameter is optional, use the regular expression ((?s).*) instead of the :parameter-name syntax. For example:

    <Route Url="/Test5/((?s).*)" Method="GET" Call="Test"/>
    

URLMap with <Map> Elements

InterSystems IRIS compares the incoming URL to the prefix in each <Map> element in the URL map. It forwards the incoming REST call to the REST service class specified in the first matching <Map> element. That class processes the remainder of the URL, typically calling the method that implements the service. The <Map> element has two attributes:

  • Prefix — specifies the segment of the URL to match. The incoming URL typically has other segments after the matching segment.

  • Forward — specifies another REST service class that will process the URL segments that follow the matching segment.

Consider the following URLMap that contains three <Map> elements.

XData UrlMap
{
  <Routes>
    <Map Prefix="/coffee/sales" Forward="MyLib.coffee.SalesREST"/>
    <Map Prefix="/coffee/repairs" Forward="MyLib.coffee.RepairsREST"/>
    <Map Prefix="/coffee" Forward="MyLib.coffee.MiscREST"/>
  </Routes>
}

This UrlMap forwards the REST call to one of three REST service classes: MyLib.coffee.SalesREST, MyLib.coffee.RepairsREST, or MyLib.coffee.MiscREST.

The complete REST URL to call one of these REST services consists of the following pieces:

  • The base URL

  • Name of the web application as defined on the Web Application page (click System Administration > Security > Applications > Web Applications). For example, the web application for these REST calls could be named /coffeeRESTSvr

  • The Prefix for a <Map> element.

  • The remainder of the REST URL. This is the URL that will be processed by the REST service class that receives the forwarded REST request.

For example, the following REST call:

http://localhost:52773/coffeeRESTSvr/coffee/sales/reports/id/875

matches the first <Map> with the prefix /coffee/sales and forwards the REST call to the MyLib.coffee.SalesREST class. That class will look for a match for the remainder of the URL, "/reports/id/875".

As another example, the following REST call:

http://localhost:52773/coffeeRESTSvr/coffee/inventory/machinetype/drip

matches the third <Map> with the prefix /coffee and forwards the REST call to the MyLib.coffee.MiscREST class. That class will look for a match for the remainder of the URL, "/inventory/machinetype/drip".

Note:

In this URLMap example, if the <Map> with the prefix="/coffee" was the first map, all REST calls with /coffee would be forwarded to the MyLib.coffee.MiscREST class even if they matched one of the following <Map> elements. The order of the <Map> elements in <Routes> is significant.

Specifying the Data Format

You can define your REST service to handle data in different formats, such as JSON, XML, text, or CSV. A REST call can specify the form that it expects data it is sending by specifying a ContentType element in the HTTP request and can request the return data format by specifying an Accept element in the HTTP request.

In the DocServer sample, the GetNamespaces() method checks if the REST call requested JSON data with the following:

If $Get(%request.CgiEnvs("HTTP_ACCEPT"))="application/json"

Localizing a REST Service

Any string value returned by a REST service can be localized, so that the server stores multiple versions of the strings in different languages. Then when the service receives an HTTP request that includes the HTTP Accept-Language header, the service responds with the appropriate version of the string.

To localize a REST service:

  1. Within your implementation code, rather than including a hardcoded literal string, use an instance of the $$$Text macro, providing values for the macro arguments as follows:

    • The default string

    • (Optional) The domain to which this string belongs (localization is easier to manage when the strings are grouped into domains)

    • (Optional) The language code of the default string

    For example, instead of this:

     set returnvalue="Hello world"
    

    Include this:

     set returnvalue=$$$TEXT("Hello world","sampledomain","en-us")
    
  2. If you omit the domain argument to $$$Text macro, also include the DOMAIN class parameter within the REST service class. For example:

    Parameter DOMAIN = "sampledomain"
    
  3. Compile the code. When you do so, the compiler generates entries in the message dictionary for each unique instance of the $$$Text macro.

    The message dictionary is a global and so can be easily viewed (for example) in the Management Portal. There are class methods to help with common tasks.

  4. When development is complete, export the message dictionary for that domain or for all domains.

    The result is one or more XML message files that contain the text strings in the original language.

  5. Send these files to translators, requesting translated versions.

  6. When you receive the translated XML message files, import them into the same namespace from which the original was exported.

    Translated and original texts coexist in the message dictionary.

  7. At runtime, the REST service chooses which text to return, based on the HTTP Accept-Language header.

For more information, see String Localization and Message Dictionaries.

Enable Web Sessions with REST

For an introduction, see Using Web Sessions with REST.

To enable your REST service to use a single web session over multiple REST calls, set the UseSession parameter to 1 in your REST service class:

Parameter UseSession As Integer = 1;

Note that if you choose to use a session, the system uses a CSP license until the session is ended or expires and the grace period has been satisfied. If you use the default setting for UseSession (which is 0), then the behavior is identical to that of SOAP requests, which hold a license for 10 seconds.

Supporting CORS

For an introduction, see Supporting CORS in REST Services. Note that the details of supporting CORS are slightly different when you create a web service manually as described in this appendix.

Modifying a REST Service to Use CORS

To specify that your REST service supports CORS, modify the REST service class as follows and then recompile it.

  1. Specify a value for the HandleCorsRequest parameter.

    To enable CORS header processing for all calls, specify the HandleCorsRequest parameter as 1:

    Parameter HandleCorsRequest = 1;
    

    Or, to enable CORS header processing for some but not calls, specify the HandleCorsRequest parameter as "" (empty string):

    Parameter HandleCorsRequest = "";
    

    (If HandleCorsRequest is 0, then CORS header processing is disabled for all calls. In this case, if the REST service receives a request with CORS headers, the service rejects the request. This is the default.)

  2. If you specified HandleCorsRequest parameter as "", edit the URLMap XData block to indicate which calls support CORS. Specifically, for any <Route> that should support CORS, add the following property name and value:

    Cors="true"
    

    Or specify Cors="false" in a <Route> element to disable CORS processing.

If a REST service class forwards a REST request to another REST service class, the behavior of the CORS processing is determined by the class that contains the <Route> element that matches the given request.

Overriding the CORS Header Processing

Important:

The default CORS header processing is not suitable for REST services handling confidential data.

The default CORS header processing does not do any filtering and simply passes the CORS header to the external server and returns the response. You may want to restrict access to origins in a domain allow list or restrict the allowed request methods. You do this by overriding the OnHandleCorsRequest() method in your REST service class.

For information on implementing the OnHandleCorsRequest() method, see Defining OnHandleCorsRequest().

Note that all URL requests that match <Route> elements in the UrlMap are processed with the single OnHandleCorsRequest() method that is defined in the class. If you need different implementations of the OnHandleCorsRequest() method for different REST URL requests, you should use Forward to send the requests to other REST service classes.

Variation: Accessing Query Parameters

The recommended way to pass parameters to a REST service is to pass them as part of the URL path used to invoke the service (for example, /myapi/someresource/parametervalue). In some cases, however, it may be more convenient to pass the parameters as query parameters (for example, /myapi/someresource?parameter=value). In such cases, you can use the %request variable to retrieve the parameter values. Within a REST service, the %request variable is an instance of %CSP.RequestOpens in a new tab that holds the entire URL query. To retrieve the value of a given query parameter, use the following syntax:

$GET(%request.Data(name,1),default)

Where name is the name of the query parameter and default is the default value to return. Or, if the same URL holds multiple copies of the same query parameter, use the following syntax:

$GET(%request.Data(name,index),default)

Where index is the numeric index of the copy you want to retrieve. For further details, see the class reference for %CSP.RESTOpens in a new tab.

Example: Hello World!

The following code fragments represent an extremely simple sample of a REST service. There are three classes: helloWorld.disp, helloWorld.impl, helloWorld.hwObj. While helloWorld.disp and helloWorld.impl extend %CSP.RESTOpens in a new tab to establish the REST service, helloWorld.hwobj extends both %PersistentOpens in a new tab and %JSON.AdaptorOpens in a new tab, which are helpful when using a POST method to create an object.

Class helloWorld.disp Extends %CSP.REST
{

Parameter HandleCorsRequest = 0;

XData UrlMap [ XMLNamespace = "https://www.intersystems.com/urlmap" ]
{
<Routes>
    <Route Url="/hello" Method="GET" Call="Hello" />
    <Route Url="/hello" Method="POST" Call="PostHello" />
</Routes>
}

ClassMethod Hello() As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        Set response=##class(helloWorld.impl).Hello()
        Do ##class(%REST.Impl).%WriteResponse(response)
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}

ClassMethod PostHello() As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        Set response=##class(helloWorld.impl).PostHello(%request.Data)
        Do ##class(%REST.Impl).%WriteResponse(response)
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}

Note the structure of the above dispatch class: A URLMap is defined with different end points for different methods and there are class methods that dispatch those routes to an implementation class. Along the way, the class performs some error handling. Read more about reporting errors and setting status codes in the %REST.ImplOpens in a new tab documentation. Class methods that are endpoints for POST or PUT methods will need to use the special %requestOpens in a new tab parameter and pass information from said parameter on to the implementation method.

Class helloWorld.impl Extends %CSP.REST
{

ClassMethod Hello() As %DynamicObject
{
    Try {
       return {"Hello":"World"}.%ToJSON()
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("500")
        return {"errormessage": "Server error"}
   }
}

ClassMethod PostHello(body As %DynamicObject) As %DynamicObject
{
    Try {
        set temp = ##class(helloWorld.hwobj).%New()
        Do temp.%JSONImport(body)
        Do temp.%Save()
        Do temp.%JSONExportToString(.ret)
        return ret
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("500")
        return {"errormessage": "Server error"}
   }
}

}

The above sample implementation class has two methods, one that simply returns a “Hello World” message formatted as a JSON and one that creates an object based on some inputs and then returns the contents of said object. See the documentation for %PersistentOpens in a new tab and %JSON.AdapterOpens in a new tab for more information about the use of methods like %New() and %JSONImport(), respectively.

Although the helloWorld.hwobj class could have many properties, for the sake of simplicity in this example, it only has one:

Class helloWorld.hwobj Extends (%Persistent, %JSON.Adaptor)
{

Property Hello As %String;

Storage Default
{
<Data name="hwobjDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Hello</Value>
</Value>
</Data>
<DataLocation>^helloWorld.hwobjD</DataLocation>
<DefaultData>hwobjDefaultData</DefaultData>
<IdLocation>^helloWorld.hwobjD</IdLocation>
<IndexLocation>^helloWorld.hwobjI</IndexLocation>
<StreamLocation>^helloWorld.hwobjS</StreamLocation>
<Type>%Storage.Persistent</Type>
}

}

After configuring a Web Application in the Management Portal by following the instructions in Basics of Creating a REST Service ManuallyOpens in a new tab, use Postman or any other REST client to send requests to the Web Application and see its responses.

FeedbackOpens in a new tab