Arne Bahlo

I18n in Node.js

Yesterday we added unit tests for a component that uses the Intl API to a frontend project. Everything worked flawlessly on our local machines, but it failed on CI. The failing tests showed a number formatted in English instead of the expected German format.

We use Jest for running the tests, which is running on Node.js. As we found out in the process, Node.js uses ICU (International Components for Unicode) for its i18n support and, if not otherwise specified, only contains English ICU data.

The reason it worked on our local machines was, that they had node installed with full ICU data (Homebrew, for example, installs all by default), but the Docker image we used on CI didn’t.

Solution

To have full ICU data available, you can either

  • compile the node binary with the flag --with-intl=full-icu (see Options for building Node.js), or
  • install the full-icu package and use an environment variable (NODE_ICU_DATA=node_modules/full-icu) or a command-line flag (--icu-data-dir=node_modules/full-icu) to tell node where to find the data (see Providing ICU data at runtime).

Since it was only failing on CI, we updated the build script to install the full-icu package and export the environment variable. Also, in case you wondered, all major browsers support the Intl-API, according to Can I use.

Conclusion

If you use any i18n features in your JavaScript application, make sure to include a basic test to be sure all locales you need are supported.

On Hexagonal Architecture

A good architect maximises the number of decisions not made

β€” Robert C. Martin in Clean Architecture

Most web services I worked with use a MVC-style architecture, with a handlers package and, if at all, a repository package. While this may be great for small services, the handlers package introduces a big problem: It mixes transport logic with business logic. This makes refactoring hard (just imagine switching your HTTP framework) and therefore forces you to make decisions about these kind of things before even starting the project. So when I started a new project recently, I decided to use the hexagonal architecture (aka Ports and Adapters) and so far I’m really happy.
Here’s what I like about it:

All dependencies point inward

All outer layers like transport, storage or logging depend on the business logic, but never the other way around. The business logic is agnostic of any other layer, it doesn’t care, how it’s served or how data is stored, it’s pure code. This makes changing it super easy.

You can defer decisions

You can defer many decisions about technologies used until you really need them. You could, for example, start out with an inmem package for storage and only decide which database to use when you really need persistence.

Refactorings are easy

Since everything is contained in it’s domain, refactoring the transport package, for example, is just refactoring transport code. There is pure separation of concerns and everything has a clear place.

Testing business logic is easy

Since you only have pure code without layer dependencies, you can easily inject an inmem package as storage for example. No need for mocking complex database structs, which just cost time.

Why a hexagon

It actually doesn’t matter how many sides the shape has. The point is, that there are many. The center represents the business logic and every side represents a port into or out of our application (that’s why it’s also called Ports and Adapters).

A real-world example

For this post let’s just assume that we’re building an API to manage an inventory of some sort – it’s still similar to the application I’m building. You should be able to list all items in the inventory and logged-in users should have basic CRUD access.

To set things up, I created these packages:

  • user: Domain logic for user management (login, logout, validate)
  • inventory: Domain logic for inventory (list, create, read, update, delete)
  • inmem: In-memory storage for user and inventory
  • http: HTTP transport logic, does de-/encoding and request handling

The user and inventory packages define an interface for the storage struct. This way we can inject any struct that implements that interface and keep the separation of concerns.

Here’s how our main.go would look like, simplified:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"net/http"

	transport "github.com/bahlo/inventory/http"
	"github.com/bahlo/inventory/inmem"
	"github.com/bahlo/inventory/inventory"
	"github.com/bahlo/inventory/user"
)

func main() {
	userStorage := inmem.NewUserStorage()
	userService := user.NewService(userStorage)
	http.Handle("/v1/user", transport.UserHandler(userService))

	inventoryStorage := inmem.NewInventoryStorage()
	inventoryService := inventory.NewService(inventoryStorage)
	http.Handle("/v1/inventory", transport.InventoryHandler(inventoryService))

	http.ListenAndServe(":8080", nil)
}

Problems

In real-world applications, we always have to make compromises and fight for clear separation of concerns. This is a list of problems I ran into while building the application.

Authentication

Let’s say we want to implement a token-based authentication and we need to protect some routes of the inventory service. Where should we get the token-validation function from? Getting the user service via our NewService constructor would result in unnecessary dependencies.
What I ended up doing was a http.Guard(userService) function, which returned a http middleware (func (next http.Handler) http.Handler) which parses the token and validates with userService.UserForToken. The middleware is then passed into the transport.InventoryHandler and wrapped around methods that needed protection. This way, there are no new dependencies.

Model decorators

Another problem are Go tags (read up). Let’s say we need some for transport (e.g. json:"id") and database (e.g. db:"first_name"), but they’re attached to the model, which lies in the domain logic. A fix would be having the model structs duplicated in the other packages with the needed tags, but this introduces a lot of duplicate code and unnecessary complexity, so I decided to leave it as-is right now until I found a better solution.

Conclusion

I’m really happy with my application and I don’t think I’ll go back to MVC-style applications any time soon.
I encourage you to try and build a service using a hexagonal architecture and share your experience – obviously doesn’t have to be Go. If you want to read more on hexagonal architecture, I recommend checking out go-kit. There is also a GopherCon 2018 talk by Matt King, here is the script until the videos are up. Also if you have comments or suggestions, hit me up πŸ€™πŸ».

Hello World

This is not a post, please move along. Check out my about page to learn more about me, subscribe and expect a new post any day.