Building ElleXX

Ellexx is a website aimed towards women, to close the financial gap. It is run by journalists that write a lot of useful content on the topic. A topic that I - as a woman in Switzerland - felt honoured to work on, and that is close to Liip values. The most important criterion of elleXX was to be able to publish content quickly and easily. There are also some integrations with products that support the user in their financial decisions. Being able to keep track of the members of the site was also important.

For content management, we chose to go with Ghost CMS. Mainly because Ghost works really well for our needs: Content. The second reason is that Ghost is funded 100% by its users; it's an open-source CMS!

For keeping track of leads and contacts we went with friendly automatic. For one strong reason: It's a local company in Switzerland. It is also built on top of Matic, which is a well known framework for marketing solutions.

We also needed custom integrations with Vontobel, MigrosBank and Cap Reschtschutz. For this, we built our own API using the JS framework nest.js. Nest.js makes it easy and fast to build an API; it lets us as developers focus on the logic of the API and less about things such as routing etc. We then use the API of our Ghost instance.

Ghost wasn't as flexible as we would have wanted when it came to the custom features on the website, because of this we had to fork ghost and maintain our own copy. For instance: The customer wanted to make displaying some content easier in a uniform way. Forking a codebase is something that you in general try to avoid, but after doing this we gained a lot of flexibility we wouldn't have had otherwise.

REST API design with Nest.js

Nest is a node.js framework that makes it straightforward to build scalable server-side applications. Being a PHP developer, I chose nest over a PHP solution that I'm used to because the team I would be working with is more comfortable with JavaScript. The most important factor for me when choosing a technology is how easy it is for people that have to maintain it. AND we're really happy that we chose this framework! It really was a lot of fun to build an API with it.

Nest is divided in modules, each module has its own configuration, and also its own controllers and services/providers. For our project we chose to make a separate module for every integration, so a Vontobel module, a MigrosBank module and a Cap Rechtschutz module. We also added a few other modules that we needed, such as "Auth", "Mail", "Ghost", "Customer" ...

This is an entire controller that we built in nest to create a new ghost member:

import { Controller, Post, Body, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { GhostService } from './ghost.service';

@Controller('ghost')
@ApiTags('ghost')
export class GhostController {
  constructor(private readonly ghostService: GhostService) {}

  @Post('new')
  async newMember(@Body() body): Promise<any> {
    return this.ghostService.getGhostMemberUuidFromEmailCreateNewMemberIfNotExists(
      body.firstname,
      body.email,
    );
  }
}

We use a lot of annotations in nest, that makes programming easy and straight forward. Note the use of @Controller to note that this is a controller @ApiTags for open API documentation, and @Post for the route!

The code injection works because of the configuration in the module:

import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { GhostController } from './ghost.controller';
import { GhostService } from './ghost.service';

@Module({
  imports: [HttpModule],
  controllers: [GhostController],
  providers: [GhostService],
})
export class GhostModule {}

Note how we there have the "GhostService" as a provider, which is a service that we created ourselves. The ghost service has methods for communicating with ghost. We use this to connect ghost members with the API members for our integrations, and also to add members to friendly automatic as soon as they sign up.

TypeScript is fully typed which also gives us the benefit of beautiful documentation with Open API by using objects for API responses. In the objects you annotate the properties for open API to output documentation like so:

@ApiProperty({
    required: true,
    description:
      'The id that the frontend calling the ellexx api is using to identify their user, is usually a unique identifier',
    example: 'sdfsd-23432-sfdfsd-2323',
  })
  public frontendId: string;

Setting up open api this way takes little effort but gives an invaluable documentation for API consumers.

Infrastructure

For hosting we wanted a swiss solution and decided to use managed Kubernetes from exoscale, we also use longhorn for MySQL. It's definitely overkill for our project to use kubernetes, but it works wonderfully. We use Gitlab to host our code and gitlab CI to automate our workflow. On pushing code an image is created and deployed. To stage for the stage branch and to prod for the main branch. Our kubernetes solution has the advantage that it's scalable. HOWEVER; Ghost is not scalable. For a high traffic Ghost website you should solve that by using http cache. We have setup a simple varnish cache for just in case. The API however; is scalable. Due to the dynamic nature of the endpoints we can't take much advantage of a cache here.

Post-Mortem

Using Ghost CMS, nest, friendly analytics, gitlab, gitlab CI, kubernetes and mysql with longhorn, would we have changed anything? No, not really! We are really happy with our setup. The only thing that we could potentially have done in a different way is that we could have used ghost as headless CMS instead, but in the end - It worked well this way too, and we are all very happy and proud of what we have accomplished.