RESTful API

Beginner’s guide to RESTful Application Program Interface (API) design and documentation. Through the example you will learn some common pattern for RESTful API.

Before you read

You need to know

  • How to create RESTful web service in Ensemble
  • How to consume RESTful web service in Ensemble
  • How to pass service parameters
  • How to return service result

What is a Service API?

What is an Application Programming Interface? Is it something materialized? Is it a single programming unit? What is an API for? From my point of view an API is something which is determined by the program code in an indirect way. But the fully defined API is provided by a container (controlled by deployment settings) running the executable program. So I rather define the API as the public description of a service. The description could be either human readable or for robots only. Or both. An API is for sharing essential information about a service with those who are about to consume it. An API explains what the service for, the context in which it can be used, what the functions are, what data structures are managed etc.

In the good old days the “program documentation” was more or less “a necessary evil”. The modern programming languages were forcing kind of a documentation by introducing declarations in the program source. Although declarations were the “robot” readable documentation, by using tools (runoff, Java doc…) you could extract information and format it for humans. Even if no single line of true documentation was add to the source, these tools were still able to produce some minimal text.

Is it somewhat different these days? Not really. Service API remained an abstraction what people mean by collection of information required to properly use a functional piece of computer software. There are languages to formalize the API definition like Web Services Description Language (WSDL). Unfortunately those have limited use. Not because for example WSDL is not capable enough to express a RESTful API, but because of non-technical mismatch. (How does it look like expressing a JSON structure in XML?) At the end there is no such de-facto standard language for REST as WSDL for SOAP Web Services. Pity. Is not it?

Never mind. Anyhow, first we need to understand what an API is documenting.

What an API is made of?

What are the core attributes of a service.

  • Service location. The URL root path of the service. Like http://localhost:57774/csp/msa/person.
  • Service methods. Those are the functions of a service. A method is defined by combination of the verb from the HTTP header (GET, POST, PUT…) and the additional path type parameters.
  • Accepted method parameters. A list of parameter with its type. The type can be path for parameters included in the URL path; query for URL query encoded; form for form data; content for HTTP message body.
  • Return status. The status field from the HTTP response header. There might be several return status codes per service method. The number is depending on the service method and the granularity of exception handling.
  • Response contents. The expected contents per status code. The format can be different by status code. For example on successful completion of a request a JSON serialized object is expected. In case of server error (500) a plain text explanation is sent.

Example API

First let us try do describe what are we trying to achieve. We are going to build a very simple service. A registry service. It is going to manage resources of the same type. For example persons.

The structure is very simple: a name, date and place of birth, mother’s maiden name and a generated internal unique registry ID. Place has a structure like: country, city.

We would like to insert a new record into our registry (update full registry entry), update individual attributes of an entry (update attribute), delete an entry, get a single entry by registry ID, query a list of registry IDs based on attribute match.

We also want some service function: initialize a registry, populate some records for testing.

From the external world we would like http://localhost:57774/csp/msa/person as service location.

Adding a new entry via PUT on the service location. Sending the registry record as a content. On return the complete entry with registry ID is expected.

Update via POST. The URL is complete with the registry ID of the entry to be updated. The attributes to be updated are sent as form data.

GET is used to retrieve data. If the URL path ends with an ID, than the registry entry identified by the ID is returned. If no ID found but there is a query in the URL, than a list of IDs is returned. For example http://localhost:57774/csp/msa/person/12A33 returns the entry 12A33. The query key value pairs are the attribute matching clauses used internally for selecting the entries. For example http://localhost:57774/csp/msa/person?name=Hahn%20Istvan&dob=1961 returns a list of persons who was born in 1961 with the name Hahn Istvan.

DELETE does delete.

POST to http://localhost:57774/csp/msa/person/_init will initialize the registry.

POST to http://localhost:57774/csp/msa/person/_populate/100 loads 100 of test entries.

API Documentation

The following section gives an example how the service API could be documented. Please remember, that neither the structure nor the contents are standardized. It is just an example.

I tried to make the documentation “tool agnostic”. There are documentation tools on the market. Some of them do their job almost as good as it ought to be. The intent of this section is to give you a feeling what the complexity of an API documentation is if you do it with a text editor.

Resource:             person

A generic service to manage person type resources. The person has a minimum set of attributes. Basically demographics and a registry ID.

Location:     http://localhost:57774/csp/msa/person

Method:

Get a single resource based on the unique ID of the resource.

Verb: GET

Parameters:

Name

Type

Data type

Comment

1

Path

Resource ID

Unique ID of the resource to be retrieved.

Response:

Status

Return type

Comment

200

Person

Record found.

204

None

No record with Resource ID exists.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Get a list of matching resource IDs based on non-unique query. The method uses the query part to build the query string. The query key/ value pairs are translated to column name/ value pairs.

Verb: GET

Parameters:

Name

Type

Data type

Comment

name

query

string

Search criteria.

motherMaidenName

Query

String

 

dob

Query

Date

 

birthPlaceCounty

Query

String

 

birthPlaceCity

Query

string

 

Response:

Status

Return type

Comment

200

Person

Record found.

204

None

No matching record.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Delete an entry from the registry.

Verb: DELETE

Parameters:

Name

Type

Data type

Comment

1

path

string

Unique registry ID.

Response:

Status

Return type

Comment

200

Person

Record deleted.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Add or update an entry to the registry.

Verb: PUT

Parameters:

Name

Type

Data type

Comment

None

content

JSON

An object serialized to JSON format.

Response:

Status

Return type

Comment

200

Person

The entry with the generated resource ID either newly add or updated by the registry service.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Update individual attributes of a registry entry.

Verb: POST

Parameters:

Name

Type

Data type

Comment

 

1

Path

String

The resource ID of the entry to be updated.

 

name

Form

string

New value of the attribute

motherMaidenName

Form

String

 

dob

Form

Date

 

birthPlaceCounty

Form

String

 

birthPlaceCity

Form

string

 

Response:

Status

Return type

Comment

200

Person

Record updated.

204

None

No record with Resource ID exists.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Initialize the registry.

Verb: POST

Parameters:

Name

Type

Data type

Comment

_init

Path

 

 

Response:

Status

Return type

Comment

200

None

Initialized.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Method:

Populate test data.

Verb: POST

Parameters:

Name

Type

Data type

Comment

_populate

Path

 

 

2

Path

Numeric

Number of entries to be populated.

Response:

Status

Return type

Comment

200

None

Initialized.

401

None

Unauthorized access. The resource needs user credential in the header.

403

None

Forbidden. User is not authorized to access the resource.

500

Error

Internal server error.

501

Error

Requested method is not implemented.

503

Error

Service is temporarily unavailable.

 

Data structures:

Person

Name

Type

Flag

Comment

ID

RegistryID

R

Generated registry ID.

Name

String

R

Name of the person in the person’s native language form.

DOB

Date

R

Date of birth.

BirthPlace

BirthPlace

O

Birth place.

MotherMaidenName

String

O

Mother’s maiden name.

 

BirthPlace

Name

Type

Flag

Comment

Country

String

O

Country code.

City

String

R

City name

 

Error

Name

Type

Flag

Comment

Code

String

R

Error code

Text

String

O

Error text

InnerError

Error

O

An inner error reported by the subcomponent of the reporting component.

 

Implementation

The following section gives an example for the resource registry we discussed earlier. This is (again) just an example.

To make you understand easier, I group the source in an artificial way.

n  Everything belonging to the API is squeezed into the resource map class.

n  The complete UrlMap XData block is sliced into single Route entries.

n  Each entry is glued to the static method actually implementing the functionality.

So to recover the true class needs some (re-)engineering. Please, happy (re-)engineering!

The first service method is the query…

 

<!-- Query the registry. The URL query part holds select criteria. -->

<Route Url="/:service" Method="GET" Call="QueryRegistry"/>

classmethod QueryRegistry(service) as %Status {     

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.runQuery(..getQueryParameters($listbuild("name","dob","motherMaidenName","birthPlaceCountry","birthPlaceCity"))))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

<!-- Get a single entry -->

<Route Url="/:service/:registryID" Method="GET" Call="GetEntry"/>

classmethod GetEntry(service,registryID) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.get(registryID))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

 

<!-- Delete a single entry -->

<Route Url="/:service/:registryID" Method="DELETE" Call="DeleteEntry"/>

classmethod DeleteEntry(service,registryID) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.delete(registryID))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

 

<!-- Utility method to initialize the registry. -->

<Route Url="/:service/_init" Method="POST" Call="InitializeRegistry"/>

classmethod InitializeRegistry(service) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.init())

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

<!-- Utility method to populate test data. -->

<Route Url="/:service/_populate/:numberOfRecords" Method="POST" Call="Populate"/>

classmethod Populate(service,numberOfRecords) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.populate(numberOfRecords))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

 

<!-- Update individual attributes of a registry entry. -->

<Route Url="/:service/:registryID" Method="POST" Call="UpdateAttribute"/>

classmethod UpdateAttribute(service,registryID) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.updateAttribute(registryID, ..getFormParameters($listbuild("name","dob","motherMaidenName","birthPlaceCountry","birthPlaceCity"))))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

 

<!-- Add a new or update an existing registry entry. -->

<Route Url="/:service" Method="PUT" Call="AddOrUpdate"/>

classmethod AddOrUpdate(service) as %Status {

        try {

               set serviceInstance = ..getServiceInstance(service)

               do ..dumpResponse(serviceInstance.addOrUpdate(..getContentParameter()))

        }

        catch ex {

               do ..ReportHttpStatusCode(..getHTTPStatusCode(ex),ex.AsStatus())

        }

        quit $$$OK

}

 

Now this is time to share you the utility methods.

 

classmethod getServiceInstance(serviceName) as Ens.BusinessService {

        set status = ##class(Ens.Director).CreateBusinessService(serviceName, .instance)

        throw:$$$ISERR(status) ##class(NoProduction).%New(status)

        quit instance

}

 

classmethod getHTTPStatusCode(ex) {

        quit $case(ex.%ClassName(1),

                              ##class(NoProduction).%ClassName(1)                  :503,

                              ##class(NotImplemented).%ClassName(1)        :501,

                                                                                                                 :500)

}

 

classmethod dumpResponse(responseObject) {

        if $isObject(responseObject) {

               if responseObject.%Extends(##class(%DynamicObject).%ClassName(1)) { write responseObject.%ToJSON() }

               elseif responseObject.%Extends(##class(%ZEN.proxyObject).%ClassName(1)) {

                       do ##class(%ZEN.Auxiliary.jsonProvider).%ObjectToJSON(responseObject)

               }

               elseif responseObject.%Extends(##class(%XML.Adaptor).%ClassName(1)) {

                       do responseObject.XMLExportToString(.ret)

                       write ret

               }

               else { throw ##class(Serialization).%New() }

        }

        else {

               write responseObject

        }      

}

 

classmethod getQueryParameters(parameterList) as %DynamicObject {

        set parameterObject = {}

        for i=1:1:$listlength(parameterList) {

               set parameterName=$listget(parameterList,i)

               set $property(parameterObject, parameterName) = %request.Get(parameterName)

        }

        quit parameterObject

}

 

classmethod getFormParameters(parameterList,queryObject) as %DynamicObject {

        if $data(queryObject) { set parameterObject = queryObject }

        else { set parameterObject = {} }

        for i=1:1:$listlength(parameterList) {

               set parameterName=$listget(parameterList,i)

               set $property(parameterObject, parameterName) = %request.Get(parameterName)

        }

        quit parameterObject

}

 

classmethod getContentParameter() as %DynamicObject {

        quit {}.%FromJSON(%request.Content)

}

 

And this is the end. We finished designing(?), implementing and documenting a RESTful Web Service API.

 

Stay tuned, I’ll be back soon with further reading on Ensemble RESTful web services. The next is “Creating an Ensemble MicroService using RESTful  Web Services”.

 

 

  • + 2
  • 0
  • 1946
  • 0