Express vs Jolie: Creating a REST Service

Jolie is a service-oriented programming language, developed for the purpose of syntactically expressing some of the key principles of service-oriented programming. Express is a leading framework in the world of JavaScript for building REST services.

The concepts behind Express/JavaScript and Jolie are not that far apart, but they are introduced under different shapes: one aims to build on top of JavaScript and Node.js, the other aims to express these concepts declaratively To do. The purpose of this article is to identify the first differences that arise between Express and Jolie in REST service development, by comparing how similar concepts are codified.

We put our comparison in a concrete example: a simple REST service that exposes an API to retrieve information about users. Users are identified by username and are associated with data that includes the name, e-mail address and an integer representing the “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 usernames of users in the system, with the possibility to filter them by karma. For example, to get a list of usernames associated with a minimum karma 5, we can 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 some library modules from requireAnd then we create the application (const app = express(),

To represent data about users, we use a simple object (users) that associates usernames (as properties) with objects with names, e-mail addresses, and deeds. We have two users, identified by username respectively john And jane,

Then we add two routes to our application:

  1. The first route is to obtain information about a specific user. We start by validating the request and return immediately if there is a validation error. 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.
  2. The second route is to list users. The boilerplate for validation and error checking is the same. We use a for loop to find the usernames of users with enough karma and store them in an array, which is then returned to the caller.

After configuring our application, we launch it by listening on TCP port 8080.

jolie

Now we apply the same example to 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 (type and interface) of the service, with two functions for listing and viewing users. Then, we a. Let’s define the implementation of your service using service block, which is configured to handle client requests concurrently (execution: concurrent,

input port Web Defines an HTTP access point for the client, and contains a protocol configuration that maps our two operations to the expected HTTP resource path (using the URI template), method, and status code.

The data structure about users for service initialization is defined in blocks (init, << Operators are used in Jolie to make deep copies of the data tree.

The behavior of the service is defined by main procedure, which provides a choice ([..]{..} [..] {..}) between our two functions.

The same operation is implemented in logic as seen in the previous JavaScript code snippet. In ViewUser, if the requested username cannot be found, we make an error (UserNotFound) –users.(request.username) roughly translates to jolie users[request.username] in Javascript.

comparison

From this simple example, we can see some interesting differences. We focus on concepts by omitting minor details or aesthetic differences from discussion.

API-First and Validation

Jolie code starts by explicitly defining the API of the service, whereas in Express the API is implicitly given by routes created on the Application object. (Message types can indeed be omitted in Jolie, but it is considered best practice to include them.)

We can say that Jolie follows an “API-first” approach, whereas in Express API a service can be defined after it has been coded (or not at all, even).

There are two important features that Jolie provides out-of-the-box if you provide the message type.

  • runtime check: All messages are checked at runtime to respect their expected types. So it is not necessary to write code to validate the request in Jolie.
  • automatic casting, The parameters in the querystring are automatically converted from strings to their expected types. If casting is not possible, an error is automatically returned to the client. in express, req.query.minKarma is a string, which JavaScript can automatically convert to a number if needed. To check that this won’t be a problem, we need to add the verification code query('minKarma').isNumeric(),

Manual versions of these features can be implemented in Express applications. For example, one can use express-validator middleware in the configuration of an application to check that messages respect certain JSON schemas. Jolie provides additional integrated mechanisms for performing validation, for example refinement types and service decorators. We compare the verification techniques in Express and Jolie in another article.

code structure

Defining a service requires dealing with its access point (how it can be accessed by clients) and its implementation (see also 5 Principles of Service-Oriented Programming).

In Express, these aspects are coded by the calling methods of the Application object. The approach is multi-paradigm: it combines objects, functions, and imperative programming. Service is represented as a commodity (app In our example), and the functions provided by the service are defined step-by-step along with their implementation. The definition of the service is given by the internal state of the application object (modified by implementing get method in our example).

In Jolie, the business logic of configuring and operating the access points that the service provides is kept separate. Definition of a service and its access points (service App And inputPort Web In our example, respectively) are given in a declarative approach, which syntactically reveals the structure of a service. Jolie is service-oriented: the concepts required to define a service are supported by linguistic primitives (as opposed to libraries).

The implementation of each operation in Jolie is then given in main blocks, and is pure business logic that abstracts from (and even transports) the concrete data format used by the access point. The mapping from resource paths to operations (and to fault status codes) is given in the access point’s configuration. In general, operation implementations can be reused with different access point configurations. For example, we can easily reconfigure our service to send XML responses by changing format = "json" In the configuration of the input port 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 service’s access point using subsequent method invocations, whereas in Jolie this is achieved with a more static and declarative approach. Another interesting difference lies in the approach to business logic: Jolie’s syntax naturally guides the developer towards abstracting business logic from concrete data formats.

Leave a Comment