Comparing Express With Jolie: Creating a REST Service
How Express and Jolie look like in the development of a simple REST service
Join the DZone community and get the full member experience.
Join For FreeJolie is a service-oriented programming language, developed with the aim of making some key principles of service-oriented programming syntactically manifest. Express is a leading framework in the world of JavaScript for the creation of REST services.
The concepts behind Express/JavaScript and Jolie are not that distant, but they are offered under different shapes: one aimed at building on top of JavaScript and Node.js, the other aimed at expressing these concepts declaratively. The aim of this article is to identify the first differences that arise between Express and Jolie in the development of a REST service by comparing how the same concepts are codified.
We ground our comparison in a concrete example: a simple REST service that exposes an API for retrieving information about users. Users are identified by username and associated to data that includes name, e-mail address, and an integer representing "karma" that the user has in the system. In particular, two operations are possible:
- Getting information about a specific user (name, e-mail, and karma counter) by passing its username, for example by requesting
/api/users/jane
. - Listing the usernames of the users in the system, with the possibility of filtering them by karma. For example, to get the list of usernames associated to minimum karma 5, we could request
/api/users?minKarma=5
.
We assume some familiarity with JavaScript.
Express
The following code implements our simple example with Express.
const express = require('express')
const http = require('http')
const { validationResult , query } = require('express-validator')
const app = express()
const users = {
jane: { name: "Jane Doe", email: "jane@doe.com", karma: 6 },
john: { name: "John Doe", email: "john@doe.com", karma: 4 }
}
app.get('/api/users/:username', (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (users[req.params.username]) {
return res.status(200).json(users[req.params.username])
} else {
return res.status(404).json(req.params.username)
}
})
app.get('/api/users', query('minKarma').isNumeric(), (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
let usernames = []
for (username in users) {
if (req.query.minKarma && req.query.minKarma < users[username].karma) {
usernames.push(username)
}
}
const responseBody = {}
if (usernames.length != 0) {
responseBody['usernames'] = usernames
}
res.status(200).json(responseBody)
})
app.listen(8080)
We start by importing from some library modules, using require
, and then we create the application (const app = express()
).
To represent data about users, we use a simple object (users
) that associates usernames (as properties) to objects with name, e-mail address, and karma. We have two users, respectively identified by the usernames john
and jane
.
We then add two routes to our application:
- The first route is for getting information about a specific user. We start by validating the request and returning immediately in case there are any validation errors. Then, we check if the requested username is in our
users
data object: if so, we return information about that user; otherwise, we return an error with status code 404. - The second route is for listing users. The boilerplate for validation and error checking is similar. We use a for loop to find the usernames of users with sufficient karma and accumulate them in an array, which is then returned to the caller.
After we are done configuring our application, we launch it by listening on TCP port 8080.
Jolie
We now implement the same example in Jolie.
type User { name: string, email: string, karma: int }
type ListUsersRequest { minKarma?: int }
type ListUsersResponse { usernames*: string }
type ViewUserRequest { username: string }
interface UsersInterface {
RequestResponse:
viewUser( ViewUserRequest )( User ) throws UserNotFound( string ),
listUsers( ListUsersRequest )( ListUsersResponse )
}
service App {
execution: concurrent
inputPort Web {
location: "socket://localhost:8080"
protocol: http {
format = "json"
osc << {
listUsers << {
template = "/api/user"
method = "get"
}
viewUser << {
template = "/api/user/{username}"
method = "get"
statusCodes.UserNotFound = 404
}
}
}
interfaces: UsersInterface
}
init {
users << {
john << {
name = "John Doe", email = "john@doe.com", karma = 4
}
jane << {
name = "Jane Doe", email = "jane@doe.com", karma = 6
}
}
}
main {
[ viewUser( request )( user ) {
if( is_defined( users.(request.username) ) ) {
user << users.(request.username)
} else {
throw( UserNotFound, request.username )
}
} ]
[ listUsers( request )( response ) {
i = 0
foreach( username : users ) {
user << users.(username)
if( !( is_defined( request.minKarma ) && user.karma < request.minKarma ) ) {
response.usernames[i++] = username
}
}
} ]
}
}
We start by declaring the API (types and interface) of the service, with the two operations for listing and viewing users. Then, we define the implementation of our service by using a service
block, which is configured to handle client requests concurrently (execution: concurrent
).
The input port Web
defines an HTTP access point for clients, and contains a protocol configuration that maps our two operations to the expected HTTP resource paths (using URI templates), method, and status codes.
The data structure about users is defined in the block for service initialisation (init
). The <<
operator is used in Jolie to make deep copies of data trees.
The behaviour of the service is defined by the main
procedure, which offers a choice ([..]{..} [..] {..}
) between our two operations.
The operations are implemented using a logic similar to that seen in the previous JavaScript code snippet. In viewUser, if the requested username cannot be found, we throw a fault (UserNotFound)—users.(request.username)
in Jolie roughly translates to users[request.username]
in JavaScript.
Comparison
From this simple example, we can observe a few interesting differences. We focus on concepts, leaving minor details or aestethic differences out of the discussion.
API-First and Validation
The Jolie code starts by defining explicitly the API of the service, whereas in Express the API is implicitly given by the routes created on the application object. (Message types can actually be omitted in Jolie, but it is considered a best practice to include them.)
We might say that Jolie follows an "API-first" approach, whereas in Express APIs can be defined after a service is coded (or not at all, even).
There are two important features that Jolie provides out-of-the-box if you provide message types.
- Runtime Checks: all messages are checked at runtime to respect their expected types. So writing code for validating the request is not necessary in Jolie.
- Automatic Casting: parameters in querystrings are automatically converted from strings to their expected types. If casting is not possible, an error is returned to the client automatically. In Express,
req.query.minKarma
is a string, which JavaScript can cast automatically to a number when needed. To check that this would not be a problem, we had to add the validation codequery('minKarma').isNumeric()
.
Manual versions of these features can be implemented in an Express application. For example, one can use the express-validator middleware in the configuration of the application in order to check that messages respect some JSON schemas. Jolie offers additional integrated mechanisms for performing validation, for example refinement types and service decorators. We leave a comparison of validation techniques in Express and Jolie to another article.
Code Structure
Defining a service requires dealing with its access points (how it can be accessed by clients) and its implementation (see also the 5 principles of service-oriented programming).
In Express, these aspects are coded by calling methods of the application object. The approach is multi-paradigmatic: it combines objects, functions, and imperative programming. The service is represented as an object (app
in our example), and the operations offered by the service are defined together with their implementations step-by-step. The definition of a service is given by the internal state of the application object (modified by invoking the get
method in our example).
In Jolie, the configuration of access points and the business logic of the operations that the service offers are kept separate. The definition of a service and its access points (service App
and inputPort Web
in our example, respectively) are given in a declarative approach, which makes the structure of a service syntactically manifest. Jolie is service-oriented: the necessary concepts for defining a service are supported by linguistic primitives (in contrast to libraries).
The implementation of each operation in Jolie is then given in the main
block, and is pure business logic that abstracts from the concrete data format (and even transport) used by the access point. The mapping from resource paths to operations (and faults to status codes) is given in the configuration of the access point. In general, operation implementations can be reused with different access point configurations. For example, we could easily reconfigure our service to send XML responses by changing format = "json"
in the configuration of the input port to format = "xml"
.
Conclusion
In this article, we have explored the differences between Express and Jolie in the context of a simple REST service. True to the dynamic nature of JavaScript, Express sets up the API and access point of a service by using subsequent method invocations, whereas in Jolie this is achieved with a more static and declarative approach. Another interesting difference lies in the approaches to business logic: the syntax of Jolie naturally guides the developer towards keeping business logic abstract from concrete data formats.
Opinions expressed by DZone contributors are their own.
Comments