In this tutorial, you will learn how to set up a REST backend in a very short time with Pharo. This tutotial will use some Open Data to populate the service. Our service will expose the list of Automated External Defibrillator (AED) available to the public in France. Data is provided by OpenStreetMap organisation at https://www.data.gouv.fr/fr/datasets/defibrillateurs-automatiques-externes-issus-dopenstreetmap/.
A RESTful web application exposes information about itself in the form of information about its resources. It also enables the client to take actions on those resources, such as create new resources (i.e. create a new user) or change existing resources (i.e. edit a post). When a RESTful API is called, the server will transfer to the client a representation of the state of the requested resource (any object the API can provide information about). The representation of the state is often in a JSON format but could also be in another format like XML, HTML. What the server does when the client, call one of its APIs depends on 2 things that you need to provide to the server:
- An identifier for the resource you are interested in. This is the URL for the resource, also known as the endpoint. In fact, URL stands for Uniform Resource Locator.
- The operation you want the server to perform on that resource, in the form of an HTTP method, or verb. The common HTTP methods are GET for reading a resource, POST for creating a new resource, PUT for updating or replacing an existing resource, and DELETE.
For example, fetching a specific Twitter user, using Twitter’s RESTful API, will require a URL that identify that user and the HTTP method GET.
Usage example: get the list of magazines in JSON format
GET /api/v1/magazines.json HTTP/1.1
Host: www.example.gov.au
Accept: application/json, text/javascript
Pharo Launcher is the fastest way to get a working Pharo environment: image (an object space with Pharo Core libraries) + virtual machine. Pharo Launcher is a tool allowing you to easily download Pharo core images (Pharo stable version, Pharo development version, old Pharo versions, Pharo Mooc) and automatically get the appropriate virtual machine to run these images. You can install Pharo Launcher from https://pharo.org/download. Pharo Launcher documentation is available at https://pharo-project.github.io/pharo-launcher/installation/.
-
Download the dataset at https://www.data.gouv.fr/fr/datasets/r/e153b245-a704-422b-82c3-95eb6a296a0f. Once unzipped, you will find following files:
- license.txt
- metadata.csv
- data.csv
We will only use the
data.csv
file. -
Download a fresh
Pharo 10.0 - 64bit
image through Pharo Launcher and launch it -
Clean the data We could load the data with the
NeoCSV
library but we will rather do it with pure Pharo for this tutorial. An AED information can be splitted on many lines (description with line breaks). Let's uniformize the content.
csvFile := FileLocator home / 'aed_csv' / 'data.csv'.
"Get lines from the data file without the header"
lines := csvFile contents withInternalLineEndings lines copyWithoutFirst.
" Iterate over lines and merge them when it does not begin with a number"
records := OrderedCollection new: lines size.
lines do: [ :line |
(line first isDigit or: [ line first = $- ])
ifTrue: [ records add: line ]
ifFalse: [ records atLast: 1 put: records last , line ]
].
Since our domain is about AED, let's create an AED object.
Object << #AED
slots: { #id . #latitude . #longitude . #city . #freeAccess . #accessInfo };
package: 'AA-RestTuto'
Once the AED class defined, we can generate accessors (getter and setters) automatically with the system browser (right-click on the class, then generate accessors
).
Since one record is now bound to one AED, we can build our domain objects.
records collect: [ :each | | record |
record := $; split: each.
AED new
latitude: (record at: 5);
longitude: (record at: 6);
city: (record at: 11);
freeAccess: (record at: 13) = 'oui';
accessInfo: (record at: 17);
yourself. ].
We now need to store our domain objects to made them available to our REST API. We could store them in a database but we will simply use an in-memory store for simplicity.
Object << #AEDStore
slots: { #items };
sharedVariables: { #Default };
package: 'AA-RestTuto'
The Default
shared variable (variable that is shared by all instances) will keep an instance of AEDStore, itself keeping the data: a collection of AEDs.
We will define a method to access the default store:
default
^ Default ifNil: [ Default := self new ]
To simplify the loading of the data, all the code we seen has been integrated into the AEDStore class. Import the AEDStore-init.st file to get an AEDSotre able to load data from a file (drag and drop the file onto the Pharo image): Then, we will load the data in the store:
AEDStore default loadFromFile: FileLocator home / 'aed_csv' / 'data.csv'.
Let's now take a look at the data we imported. You can open a Playground (CTRL+OP) and inspect the following expression (Do It button):
AEDStore default.
The information displayed is not very useful. With Pharo, you can adapt tools to your needs, your domain. Let's add a custom inspection method on the AEDStore:
inspectionItems: aBuilder
<inspectorPresentationOrder: 0 title: 'AED'>
^ aBuilder newTable
addColumn: (SpStringTableColumn new
title: 'Index';
evaluated: [ :each | each id ];
beNotExpandable;
yourself);
addColumn: (SpStringTableColumn new
title: 'Latitude';
evaluated: [ :each | each latitude ];
beNotExpandable;
yourself);
addColumn: (SpStringTableColumn new
title: 'Longitude';
beNotExpandable;
evaluated: [ :each | each longitude ];
yourself);
addColumn: (SpStringTableColumn new
title: 'City';
evaluated: [ :each | each city ];
sortFunction: #city ascending;
yourself);
addColumn: (SpCheckBoxTableColumn new
title: 'Free access';
evaluated: [ :each | each freeAccess ];
sortFunction: #freeAccess ascending;
yourself);
addColumn: (SpStringTableColumn new
title: 'Access info';
evaluated: [ :each | each accessInfo ];
yourself);
items: items asOrderedCollection;
yourself
This methods adds a custom tab named 'AED' when we inspect an AED instance. In the method above, we describe the UI to be displayed: a table with columns.
We will load Tealight library that includes a micro web framework (Teapot) as well as small layer on top it to ease its integration into Pharo. First, we will load Teapot (that comes with Tealight) to get an updated version that is working on the very latest Pharo version:
Metacello new
repository: 'github://demarey/Teapot/repository';
baseline: 'Teapot';
load
Then, we can load Tealight:
Metacello new
repository: 'github://astares/Tealight/repository';
baseline: 'Tealight';
load
You can find some documentation on the project page: https://github.com/astares/Tealight.
After you have the framework installed you can easily start a Tealight web server by selecting
"Tealight" -> "WebServer" -> "Start webserver"
from the Pharo world menu. Internally this starts a Teapot server with some defaults.
You can also easily stop the server from the Tealight web server menu by using "Stop webserver" or open a webbrowser on the server by using "Browse".
After you started the server you can easily access the running Teapot instance from your code or playground
TLWebserver teapot.
You can easily experiment with Teapot routes, for instance using
TLWebserver teapot
GET: '/hi' -> 'HelloWorld'.
If you point your browser to http://localhost:8080/hi you will receive your first "HelloWorld" greeting.
If you open an inspector on the Teapot instance
TLWebserver teapot inspect.
you will see that a dynamic route was added:
So you can dynamically add new routes for GET, POST, DELETE or other HTTP methods interactively.
We recommend to read the Teapot chapter of the Pharo Enterprise Book to get a better understanding of the possibilities of the underlying Teapot framework.
We will now try to get the data of an AED. Let us add a new route that will return a random AED.
TLWebserver teapot
GET: '/random' -> [ AEDStore default items atRandom ].
If you point your browser to http://localhost:8080/random you will receive "an AED" ...
What happened?
Our webserver receives an HTTP GET request on /random
URL. It then executes the associated action, getting a random AED in the store, and then provide it back in the HTTP answer. Objects cannot be transfered through HTTP. They need to be serialized. The default serialization used by our web server is the text representation of the object, i.e. the result of #printString
method applied to the object.
aed := AEDStore default items atRandom.
aed printString "'an AED'"
We now need to define a serialization for our domain object. A widely used format is JSON.
In pharo, we use the NeoJSON
library.
We will add a method neoJsonMapping:
on the AED class that will specify how to map an AED to a Json object.
A class method in Pharo can be seen as a static method in Jave (as opposed to an instance method that can only be used on an instance and not a class).
neoJsonMapping: mapper
mapper mapAllInstVarsFor: self.
Now, let's take a look at the JSON produced for an AED:
NeoJSONWriter toStringPretty: aed.
will return:
{
"id" : 6231,
"latitude" : "48.1739017998959",
"longitude" : "6.44966",
"city" : "Épinal",
"freeAccess" : false,
"accessInfo" : ""
}
We can now ask our webserver to encode objects using json as default.
TLWebserver teapot
output: TeaOutput json.
You can now point your browser to http://localhost:8080/random and you will see the difference. You can now reset the web server routes:
TLWebserver teapot removeAllDynamicRoutes.
While it is nice to experiment with dynamic routes by adding them one by one to the Teapot instance it would be even more convinient
- if we could define the REST API using regular Smalltalk methods,
- if we could map each URL easily.
To support that Tealight adds a special utility class (called TLRESTAPIBuilder) to help you easily build an API. Lets see how we can use it.
First of all we need to create a simple class in the system either from the browser or with an expression:
Object << #PharoWorkshopRESTAPI
slots: { };
package: 'AA-RestTuto'
Now we can define a class side method:
random: aRequest
<REST_API: 'GET' pattern: 'random'>
^ AEDStore default items atRandom
As you see we use a pragma in this class marking the class side method as REST API method and defining the kind of HTTP method we support as well as the function path for our REST service.
Now we can use the utility class to generate the dynamic routes for us, sending a message to our class ending up in our method:
TLRESTAPIBuilder buildAPI
This creates the dynamic routes for us.
To simplify the update of the API and do not loose the server configuration, we can define a class method on PharoWorkshopRESTAPI
to store the configuration:
configuration
^ TLWebserver defaultConfiguration copyWith: #defaultOutput -> #json
Then we will add a #build
method to simplify the building:
build
TLWebserver defaultServer configuration: self configuration.
TLRESTAPIBuilder buildAPI.
TLWebserver start. "ensure the web server is started"
Also note that by default, there is an "api" prefix generated into the URL for all REST based methods so you need to point your browser to: http://localhost:8080/api/random.
We can now add a new method to get an AED given its id. Just before, we will define another method that will provide the AED store as most of the api will need it.
store
^ AEDStore default
aed: aRequest
<REST_API: 'GET' pattern: 'aeds/<id>'>
^ self store aedWithId: (aRequest at: #id) asInteger
Before trying this new route, we need to add the #aedWithId:
method (instance side) on
AEDStore
.
aedWithId: anId
^ self items detect: [ :aed | aed id = anId ]
You can notice our pattern now includes an id parameter that will be retrieved from the URL. In a browser, we can now point to: http://localhost:8080/api/aeds/1
We can also add a route to delete an AED:
removeAed: aRequest
<REST_API: 'DELETE' pattern: 'aeds/<id>'>
^ self store removeAedWithId: (aRequest at: #id) asInteger
Before trying this new route, we need to add the #removeAedWithId:
method (instance side) on
AEDStore
.
removeAedWithId: anId
^ self items remove: (self aedWithId: anId)
To try to remove the AED with id #2, we can use curl command in a terminal. First, we will fetch the AED with #id 2.
$ curl -X GET http://localhost:8080/api/aeds/2
{"id":2,"latitude":"42.3143360005073","longitude":"9.2671767","city":"Sermano","freeAccess":false,"accessInfo":""}
$ curl -X DELETE http://localhost:8080/api/aeds/2
If you then try to get the resource, you will get a Not Found error:
$ curl -X GET http://localhost:8080/api/aeds/2
NotFound: [ :aed | aed id = anId ] not found in OrderedCollection
We can also provide in our API a way to easily locate an AED on a map.
We can do that quickly by adding a new method on the AED
object:
mapLink
^ 'https://www.openstreetmap.org/search?query=' , self latitude , ',' , self longitude, '#map=18/' , self latitude , ',' , self longitude, '&layers=N'
and by adding a new route on our API:
aedLocalisation: aRequest
<REST_API: 'GET' pattern: 'aeds/<id>/map'>
| aed |
aed := self store aedWithId: (aRequest at: #id) asInteger.
^ #map -> aed mapLink
Now, you can try to browse an AED localization at http://localhost:8080/api/aeds/1/map. Click on the provided link.
So far, we have a web-based service that handles the core operations involving AED data. But that’s not enough to make things "RESTful". In fact, what we have built is better described as RPC (Remote Procedure Call). That is because there is no way to know how to interact with this service. You would have to write a document to describe its usage. So what is missing? Hypermedia links to allow resources discovery and navigation. A side effect of NOT including hypermedia in the representation is that clients MUST hard code URIs to navigate the API. This is what is called HATEOAS: Hypermedia As The Engine of Application State.
We will define a new class that will decorate a class of our domain. It is a simple decorator that will add links to resources to an existing domain object.
Let's create a HateoasEntity
class:
Object << #HateoasEntity
slots: { #model . #links };
package: 'AA-RestTuto'
It will have a method on class side to easily instantiate it on the decorated object:
on: aModel
^ self new
initializeWith: aModel;
yourself
On instance side, we define the #initialization method as follows:
initializeWith: aModel
model := aModel.
links := Dictionary new.
We will add a method to easily add a self link:
addSelfLink: relativeUrl
links at: #self put: (self serverUrl / relativeUrl) asString
and a method to get the server base URL:
serverUrl
^ PharoWorkshopRESTAPI serverUrl
PharoWorkshopRESTAPI class >> serverUrl
is defined as follows:
serverUrl
^ TLWebserver defaultServer teapot server url / 'api'
It is the sever url of the default Teapot server we use to serve our REST resources.
We now need to ensure that our HateoasEntity
objects can be serialized in JSON since the API will now use this decorator to add links to our resources and send it as the answer to the HTPP request. We need to merge the decorator and the decorated object (domain object) into the same structure. We will use a simple dictionary that is easily convetible to JSON.
asDictionary
| dict |
dict := Dictionary new.
model class slotNames do: [ :slotName |
dict at: slotName put: (model instVarNamed: slotName) ].
dict at: #links put: links.
^ dict
To avoid the hardcoding of a domain object variables (slots), we use Pharo instrospection capabilities to add decorated object instance variable one by one to the dictionary. Last, we add all the links we want to add to this object.
We also need to define the JSON serialization of the HateoasEntity
objects. We define the mapping in a HateoasEntity
class method:
neoJsonMapping: mapper
mapper
for: self
customDo: [ :mapping |
mapping encoder: [ :entity | entity asDictionary ] ]
Let's now try to generate a JSON entity for an AED with a self link:
aed := AEDStore default items atRandom.
NeoJSONWriter toString:
((HateoasEntity on: aed)
addSelfLink: 'aed/' , aed id asString;
yourself) asDictionary
Evaluating and printing (CTRL+P) this expression in a playground, you should see something like:
{
"latitude":"48.1739017998959",
"city":"Épinal",
"accessInfo":"",
"freeAccess":false,
"longitude":"6.44966",
"id":6231,
"links":{
"self":"http://localhost:8080/api/aed/1"
}
}
We now have everything to define a route to get a list of ALL AEDs. We will not give all information on AED since data size would be too big. Instead, we will just provide a list of links to discover AEDs.
In PharoWorkshopRESTAPI class, add an #aeds:
method:
aeds: aRequest
<REST_API: 'GET' pattern: 'aeds'>
^ self store items collect: [ :aed | (self serverUrl / 'aeds' / aed id asString) asString ]
For each AED in the store, we collect the link to access the AED resource. This is the list of links that we will send back to the client. Here is a sample output if you try to access http://localhost:8080/api/aeds:
[
"http://localhost:8080/api/aeds/1",
"http://localhost:8080/api/aeds/2",
"http://localhost:8080/api/aeds/3",
"http://localhost:8080/api/aeds/4",
"http://localhost:8080/api/aeds/5",
"http://localhost:8080/api/aeds/6",
"http://localhost:8080/api/aeds/7",
"http://localhost:8080/api/aeds/8",
"http://localhost:8080/api/aeds/9",
"http://localhost:8080/api/aeds/10"
]
You can now click on any resource to perform a new rest request and discover the resource.
A good practice in REST is to provide a self link to resources that are sent to the client.
We can update our #aed:
method to include a self link:
aed: aRequest
<REST_API: 'GET' pattern: 'aeds/<id>'>
| aed |
aed := self store aedWithId: (aRequest at: #id) asInteger.
^ (HateoasEntity on: aed)
addSelfLink: 'aeds/' , aed id asString;
yourself
Here is the result of a call to a specific AED:
{
"latitude":"48.1739017998959",
"city":"Épinal",
"accessInfo":"",
"freeAccess":false,
"longitude":"6.44966",
"id":6231,
"links":{
"self":"http://localhost:8080/api/aed/1"
}
}
To build this tutorial, I used some existing resources:
- TeaLight documentation (Thank you @astares)
- A REST introduction blog post: https://medium.com/extend/what-is-rest-a-simple-explanation-for-beginners-part-1-introduction-b4a072f8740f
Libraries used in this project are:
- Teapot https://github.com/zeroflag/Teapot/
- TeaLight https://github.com/astares/Tealight
- NeoJSON https://github.com/svenvc/NeoJson
- Zinc https://github.com/svenvc/zinc/
Pharo ecosystem also have other frameworks you can use to build REST services: