Developing RESTful APIs

Late Bloomer

RAML

The RESTful API Modeling Language (RAML) [19] takes a very similar approach to OpenAPI. The initiative is backed by MuleSoft, VMware, and Cisco, among others. RAML promises to support the entire life cycle of an API – from design to rollout – and succeeds in doing so with the help of its toolset and well-prepared documentation [20].

Much like the YAML Swagger Editor, MuleSoft has developed a RAML web API Designer (Figure 3) [21]. The designed API elements can be tried out directly through a mocking service. MuleSoft loads the API design in a web service and executes it. Alternatively, MuleSoft offers an open source development environment named API Workbench [22], although it is under a proprietary license.

Figure 3: API management as a web service. You can test API elements with the help of a mocking service in the RAML API Manager.

RESTful with Ruby and Sinatra

As a simple example, I'll look at a RESTful API for managing a list of names. The RAML API Designer helped with the design. To begin, I designed the API and tested it in the RAML API Designer in a mocking environment to see whether it formally provides the right inputs and outputs. Next, I wrote the first line of code on the basis of RAML specification 0.8 (Listing 1). Thanks to the clear-cut YAML-based presentation, the developer can focus fully on API functionality, which keeps the peculiarities of a language out of the process. A tutorial [23] offers a good introduction to best practices for developing a REST API.

Listing 1

api.raml

01 #%Raml 0.8
02 title: Contacts
03 version: 0.1
04 #baseUri: http://www.rve.com/contactshelf
05 baseUri: https://mocksvc.mulesoft.com/mocks/d4c4356f-0508-4277-9060-00ab6dd36e9b/contactshelf
06 /contacts:
07   get:
08     description: Retrieve list of existing contacts
09     responses:
10       200:
11         body:
12           application/json:
13                 example: |
14                   {
15                     "data": [
16                       {
17                         "id": "1",
18                         "name": "Rheik",
19                         "link": "http://localhost:4567/contacts/Rheik"
20                       },
21                       {
22                         "id": "2",
23                         "name": "Paul",
24                         "link": "http://localhost:4567/contacts/Paul"
25                       },
26                       {
27                         "id": "3",
28                         "name": "Emil",
29                         "link": "http://localhost:4567/contacts/Emil"
30                       }
31                     ],
32                     "success": true,
33                     "status": 200
34                   }
35   /{name}:
36     get:
37       description: Retrieve a specific contact
38       responses:
39        200:
40          body:
41             application/json:
42               example: |
43                 {
44                   "data": {
45                     "id": "1",
46                     "name": "Rheik",
47                     "link": "http://localhost:4567/contacts/Rheik"
48                   },
49                   "success": true,
50                   "status": 200
51                 }
52   /new:
53     post:
54       description: Add a new contact
55       body:
56         application/json:
57             example: |
58               {
59                 "data": {
60                    "name": "Franz",
61                    "link":  "http://localhost:4567/contacts/Franz"
62                  }
63               }
64         responses:
65           200:
66             body:
67               application/json:
68                 example: |
69                    { "message": "Contact transferred correctly." }
70   /{id}:
71     delete:
72       description: Delete a specific contact
73       responses:
74         200:
75           body:
76             application/json:
77               example: |
78                 { "message": "Contact id = <<resourcePathName>> deleted." }
79         404:
80           body:
81             application/json:
82               example: |
83                 { "message": "Contact id = <<resourcePathName>> not found." }

The data for the service is stored in a SQLite database. The script in Listing 2 fills the database with three sample entries from the command line and then lists the entries.

Listing 2

SQLite Database with Sample Entries

$ sqlite3 contacts.sqlite "CREATE TABLE contacts (id INTEGER PRIMARY KEY, name, link);"
$ sqlite3 contacts.sqlite "INSERT INTO contacts (name, link) VALUES ('Rheik', 'http://localhost:4567/contacts/Rheik');"
$ sqlite3 contacts.sqlite "INSERT INTO contacts (name, link) VALUES ('Paul', 'http://localhost:4567/contacts/Paul');"
$ sqlite3 contacts.sqlite "INSERT INTO contacts (name, link) VALUES ('Emil', 'http://localhost:4567/contacts/Emil');"
$ sqlite3 contacts.sqlite "SELECT * FROM contacts;"
1|Rheik|http://localhost:4567/contacts/Rheik
2|Paul|http://localhost:4567/contacts/Paul
3|Emil|http://localhost:4567/contacts/Emil
$

The Ruby script in Listing 3 shows the program for a Sinatra server that provides a simple REST API, which is only RESTful in its approach because the JSON format used in the example is not standardized, referenced by a schema, or genuinely based on hypertext. The corresponding hypermedia schema would be used here, but it was left out of the example for the sake of clarity.

Listing 3

contacts.rb

01 require 'rubygems'
02 require 'sinatra'
03 require 'sinatra/json'
04 require 'uri'
05 require 'active_record'
06
07 ActiveRecord::Base.establish_connection(
08   :adapter => 'sqlite3',
09   :database =>  'contacts.sqlite'
10 )
11
12 class Contact < ActiveRecord::Base
13 end
14
15 get '/contacts' do
16   @contacts = Contact.all
17   @response_message = {data: @contacts, success: true, status: 200}
18   json @response_message
19 end
20
21 get '/contacts/:name' do
22   @name = params['name']
23   @contact = Contact.where(name: @name).first
24   @response_message = {data: [@contact], success: true, status: 200}
25   json @response_message
26 end
27
28 post '/contacts/new' do
29   @data = JSON.parse(request.body.read)['data']
30   puts @data.inspect
31   if @data['name'] and @data['link'] then
32     @contact = Contact.new(name: @data['name'], link: @data['link'])
33     @contact.save
34   end
35   @response_message = { "message": "Contact #{@data['name']} created." }
36   json @response_message
37 end
38
39 delete '/contacts/:id' do
40   @id = params['id']
41   @contact = Contact.find_by_id(@id)
42   if @contact then
43     Contact.delete(@contact.id)
44     @response_message = { "message": "Contact id = #{@contact.id} deleted." }
45   else
46     @response_message = { "message": "Contact id = #{@contact.id} not found." }
47   end
48   json @response_message
49 end
50
51 options '/' do
52   # Currently supported HTTP verbs
53   response.headers["Allow"] = "HEAD,GET,POST,DELETE,OPTIONS"
54   204
55 end

Easy Access

Assuming you are in the contacts.sqlite database directory, the Sinatra server can be launched by typing ruby contacts.rb. The client role is played by the versatile curl on the command line. To list the existing entries in the database, curl accesses http://localhost:4567/contacts , which then returns the data in JSON format.

The -v option displays all the HTTP parameters, from which you can specifically tell that the content type is application/json (Listing  4). Curl picks this up with curl "http://localhost:4567/contacts/Rheik", which corresponds to the get '/contacts/:name' method in the Sinatra script and /{name}: in the RAML draft.

Listing 4

Curl Access

01 $ curl -v "http://localhost:4567/contacts"
02 *   Trying ::1...
03 * TCP_NODELAY set
04 * Connected to localhost (::1) port 4567 (#0)
05 > GET /contacts HTTP/1.1
06 > Host: localhost:4567
07 > User-Agent: curl/7.51.0
08 > Accept: */*
09 >
10 < HTTP/1.1 200 OK
11 < Content-Type: application/json
12 < Content-Length: 382
13 < X-Content-Type-Options: nosniff
14 < Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21)
15 < Date: Thu, 30 Mar 2017 18:48:49 GMT
16 < Connection: Keep-Alive
17 <
18 * Curl_http_done: called premature == 0
19 * Connection #0 to host localhost left intact
20 {"data":[{"id":1,"name":"Rheik","link":"http://localhost:4567/contacts/Rheik"},{"id":2,"name":"Paul","link":"http://localhost:4567/contacts/Paul"},{"id":3,"name":"Emil","link":"http://localhost:4567/contacts/Emil"}],"success":true,"status":200}
21 $

Curl creates a new entry with a call to the POST method (Listing 5), but the program here breaks with REST logic. A more intuitive approach would be a POST to the URL of the contact resource http://localhost:4567/contacts/Jim ; however, many developers expect a fixed path (/new), if the resource does not yet exist.

Listing 5

Curl and POST

$ curl -H "Content-Type: application/json" -X POST -d '{"data": {"name": "Jim", "link": "http:://localhost:4567/contacts/Jim"}}' http://localhost:4567/contacts/new
{"message":"Successfully created contact Jim."}

It would be formally correct to send the POST to /contacts. The best idea is for developers to find a solution that catches the eye.

The call

curl -H "Content-Type: application/json" -X DELETE      http://localhost:4567/contacts/6

deletes the entry with ID 6. In contrast to the name, the ID is unique.

With the options method (Listing 3, lines 51-55), the server tells the user, on receiving an appropriate request, which HTTP verbs it supports ("Allow"). The answer requires the curl -i parameter, because the output will not otherwise appear (Listing 6).

Listing 6

Querying Options

01 $ curl -i -X OPTIONS http://localhost:4567/
02 HTTP/1.1 204 No Content
03 Allow: HEAD,GET,POST,DELETE,OPTIONS
04 X-Content-Type-Options: nosniff
05 Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21)
06 Date: Thu, 30 Mar 2017 7:11:56 PM GMT
07 Connection: Keep-Alive
08 $

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus