[SURVEY RESULTS] The 2024 edition of State of Software Modernization market report is published!
GET IT here

Building API with Express.js and Hadron

readtime
Last updated on
September 18, 2023

A QUICK SUMMARY – FOR THE BUSY ONES

TABLE OF CONTENTS

Building API with Express.js and Hadron

Introduction

Together with folks at Brainhub, we have developed a tool, with some of magic that dependency injection provides, to make it more enjoyable and easy to implement CRUD API.

Hadron is a lightweight, open-source framework that can be used with some tools like Express.js (at the moment, Express is the only supported web framework) and database management tools like TypeORM. Hadron even consists of packages like hadron-serialization, which makes it possible to simply structure the data that API outputs, even with selected fields visible only to some “groups”.

Hadron does not affect backend performance in any negative way, it’s aim is to improve the experience while building and maintaining API and providing high efficiency.

A short overview of Hadron + use cases

Setting up the backend with Hadron requires just a few lines of code. Firstly, I will show you how to set up a basic hello world route using Hadron and Express.js.

import express from 'express';
import hadron from '@brainhubeu/hadron-core';
import * as hadronExpress from '@brainhubeu/hadron-express';

const app = new express();
const port = process.env.PORT || 3000;

const config = {
 routes: {
   helloWorld: {
     path: '/',
     methods: ['GET'],
     callback: () => 'Hello world !',
   }
 }
};

hadron(
 app,
 [hadronExpress],
 config,
).then((container) => {
 app.listen(port, () =>
   console.log(`Listening on http://localhost:${port}`),
 );
})

The most important thing here is the config object. We specify there the configuration for Hadron packages (routes for hadron-express). The constructor method returns a promise with the container object, which is used for dependency injection. We will dig deeper into that later in this article.

As you’ve probably noticed, developing a larger app can be a nightmare while using more Hadron packages. The config object would be massive — just imagine all routes there. Of course, we can merely divide routes to multiple files and import them.

This is where the hadron-json-provider package comes in. This standalone package allows you to specify the path and extensions for files to be automatically imported for you.

It is quite useful for splitting our routes into different files. By default, the hadron-express package accepts key routePaths in Hadron’s config which accepts an array of paths and extensions and then uses the hadron-json-provider package to load routes from these files.

const config = {
 routePaths: [
   [
     './routes/*',
     'js',
   ],
   [
     './additionalRoutes/*',  
   ]
 ],
};

Hadron vs plain express.js

Remember that Hadron does not affect the performance of your backend application — tested using api-benchmark with 10000 runs sampled with concurrency: 10000.

Hadron provides you the space and tools that boost productivity and maintainability over your project and creates an abstract layer over frameworks like Express.js.

The main advantage of using Hadron is that you can quickly and easily select various tools to manage databases, provide security, and shape your output based on user type. Everything is simply done for you and easily configured by you.

Creating API using Hadron is so simple that you can split your routes into different files without caring about dependencies, instances, etc. This is because of dependency injection, which is used in the route’s callback function.

When you register any package, it is initialized using the provided config and put in the container under the specific key(s) that you can later use in the route’s callback functions.

Hadron’s dependency injection

Hadron provides the option to store data or object instances in something we call the container. It also keeps data initialized by Hadron packages, for example, database repositories in the hadron-typeorm package.

While registering an item in the container, you can choose one of the lifetime options, which are:

  • value — this is the default behavior. While getting data from the container, you get back the same thing that was registered.
  • singleton — always returns the same instance of registered class/constructor function.
  • transient — always returns a new instance of registered class/constructor function.

Dependency injection is used in the route’s callbacks, which allows you to access any container values easily. Let’s assume we will store a string Hello World in our container under the key message and we would like to access it on our route.

import bodyParser from 'body-parser';
import express from 'express';
import hadron from '@brainhubeu/hadron-core';
import * as hadronExpress from '@brainhubeu/hadron-express';

const port = process.env.PORT || 8080;
const expressApp = express();
expressApp.use(bodyParser.json());

hadron(
 expressApp,
 [hadronExpress],
 {
   routes: {
     basicRoute: {
       path: '/',
       methods: ['get'],
       callback: ( {}, { message } ) => `Message stored in container: ${message}`,
     },
   },
 },
).then((container) => {
 container.register('message', 'Hello World');

 expressApp.listen(port);
 console.log(`listening at http://localhost:${port}`);
});

After sending a request to http://localhost:8080/, you will receive:

"Message stored in container: Hello World"

That is how you can access container items. Most packages store items there so you can easily perform some actions or retrieve data you want.

You may now think about accessing request variables that are very often used in our routes like headers, params, body, query etc.. These data are provided as the first parameter in the callback’s function.

basicRoute: {
 path: '/',
 methods: ['get'],
 callback: ( { headers }, {} ) => JSON.stringify(headers),
},

In this case, the route’s response will return the requests’ header object.

Hadron packages quick overview

We’ve only discovered a few packages so far, especially hadron-express. Let’s discuss some of the official Hadron packages.

All examples presented in this article are available at GitHub here. Feel free to play with them.

Hadron-typeorm

This package wraps and integrates the TypeORM package with Hadron. It gives us the ability to register and access repositories or migrations the easy way.

To connect with your database, you simply import the hadron-typeorm package and provide the connection object to Hadron’s configuration.

Let’s say we have a table with users. To retrieve one record with its ID we will use an endpoint like /user/1. Our callback may look like this:

getUserById: {
 path: '/user/:id',
 methods: ['get'],
 callback: async ({ params }, { userRepository }) => ({
     body: await userRepository.findOneById(params.id),
 }),
},

Looks easy, right? But what about a connection to our database? As I mentioned before, we need to provide a connection object to our config while initializing Hadron. In the example below, we will use the MySQL database.

import { User } from './entity/User';

const connection: ConnectionOptions = {
 name: 'mysql-connection',
 type: 'mysql',
 host: 'localhost',
 port: 3306,
 username: 'root',
 password: 'my-secret-pw',
 database: 'demo-app',
 entities: [User],
};

To create a repository in TypeORM we need to provide an entity. Hadron will then register a repository from it and register under the key containing the entity name and Repository suffix — entity User will register in a container under the userRepository key.

Hadron-serialization

Serializer allows you to specify the shape of your JSON output.

Imagine a user sends a request to the API with an auth token. With a hadron-serialization package, we can easily take care of fields this user will see based on his permissions.

In the example below, there is only one endpoint
/:group? (we will take care of two groups, mod and admin).

In our case, there will be a list of users that will be returned as a response for that endpoints. Every user should see the user’s first and last name, users in the group mod should also see an email and admin will see all of them with an additional ID field.

This is the data we will use:

[
 {
   "id": 1,
   "firstName": "John",
   "lastName": "Doe",
   "email": "[email protected]"
 },
 {
   "id": 2,
   "firstName": "Adam",
   "lastName": "Kowalski",
   "email": "[email protected]"
 },
 {
   "id": 3,
   "firstName": "Rick",
   "lastName": "Svannson",
   "email": "[email protected]"
 }
]

So to make the hadron-serialization package work, we need to initialize it. But before that, we should define a schema for our data. We can define it in another file or simply provide an object literal in Hadron’s config. We will save it as an external JSON file.

{
 "name": "User",
 "properties": [
   { "name": "id", "type": "string", "groups": ["admin"] },
   { "name": "firstName", "type": "string" },
   { "name": "lastName", "type": "string" },
   { "name": "email", "type": "string", "groups": ["mod", "admin"] }
 ]
}

So, to make it clear — a schema is an object and should contain:

  • name — simply the schema’s name. We will need that to recognize the schema when serializing data.
  • properties — these field contains an array of objects. Each object holds the name of the key that will be serialized, it’s type and groups that it should be visible to (if groups are empty, they’re visible to everyone).

<span class="colorbox1" fs-test-element="box1"><p>Notice that if you don’t specify some key in a schema, it won’t be visible!</p></span>

Okay, now we have our data defined and the schema needed in the serialization package, let’s define next our Hadron instance. Our index.js file should look like this:

import express from 'express';
import hadron from '@brainhubeu/hadron-core';
import * as hadronExpress from '@brainhubeu/hadron-express';
import * as hadronSerialization from '@brainhubeu/hadron-serialization';

import data from './data.json';
import UserSchema from './schemas/User.json';

const app = new express();
const port = process.env.PORT || 3000;

const routeCallback = (serializer, group) =>
 Promise.all(
   data.map(user => serializer.serialize(user, [group], 'User'))
 ).then(users => ({
   count: data.length,
   data: users
 }));

hadron(
 app,
 [hadronExpress, hadronSerialization],
 {
   routes: {
     routeWithGroup: {
       path: '/:group?',
       methods: ['GET'],
       callback: routeCallback,
     }
   },
   serializer: {
     schemas: [UserSchema],
   }
 }
).then(container => {
 app.listen(port, () =>
   console.log(`Listening on http://localhost:${port}`),
 );
})

The above example is basically an example of an entire app made using Hadron. So it’s simple to set up the hadron-serialization package — all we need to do is to declare schema objects, which will be used to shape our data and provide them in the config.

Next when we would like to serialize our data, simply take the serializer object from the container and execute the serialize method.

Hadron-events

The Hadron-events package allows you to make use of built-in events or even declare your own custom events, bind listeners to them and simply emit them at any point of the application lifecycle.

Currently, in Hadron, there are few built-in events which are listed in the package docs, which you can find here.

To initialize our simple event handlers, we need to provide listeners to events. In the example below, we will provide the hadron-events package configuration in hadron-core’s bootstrapping function.

[...]
import listeners from './events/listeners';

hadron(
 app,
 [hadronExpress, hadronEvents],
 {
   routes: { /*[...]*/ },
   events: {
     listeners,
   }
 }
).then(container => {
 /*[...]*/
});

So we are including our listeners in an external file. In our example, we will try the handleRequestCallbackEvent, which will emit events right before every request happens.

export default [
 {
   name: 'Test',
   event: 'handleRequestCallbackEvent',
   handler: (data, ...rest) => {
     console.log('handleRequestCallbackEvent', data);
     console.log('#####');
     console.log(rest);
   }
 },
];

In the hadron-events package, we can also emit our own events at any part of application’s lifecycle. We are going to listen for 2 events with a simple name like successEvent and failEvent. We will then emit that event when user send a request to /:key, if the key has have a value of foo.

{
 customEventRoute: {
   path: '/:key?',
   methods: ['GET'],
   callback: (request, { eventManager }) => {
     const { params: { key } } = request;
     const eventToEmit = (key === 'foo' && 'successEvent') || 'failEvent';
     eventManager.emitEvent(eventToEmit)(key);
     return {
       body: {
         "key": key || '',
         "eventEmitted": eventToEmit,
       },
     };
   },
 },
}

And the last step in our example will be to implement listeners. As said before, we are going to create 2 listeners, one for successEvent and one for failEvent.

export default [
 {
   name: 'Success',
   event: 'successEvent',
   handler: (key, ...rest) => {
     console.log(`You have successfully provided value of ${key}`);
   }
 },
 {
   name: 'Fail',
   event: 'failEvent',
   handler: (key, ...rest) => {
     console.log(`You have successfully provided value of ${key}`);
   }
 },
];

That will allow us to emit these custom events from any place in our app. For simplicity in our example, that place is the request’s callback.

Links

About

Hadron and it’s official packages are maintained by the Brainhub development team. It is funded by Brainhub and the names and logos for Brainhub are trademarks of Brainhub Sp. z o.o.. You can check other open-source projects supported/developed by our teammates.

Frequently Asked Questions

No items found.

Our promise

Every year, Brainhub helps 750,000+ founders, leaders and software engineers make smart tech decisions. We earn that trust by openly sharing our insights based on practical software engineering experience.

Authors

Bianka Pluszczewska
github
Tech Editor

Software development enthusiast with 8 years of professional experience in this industry.

Read next

No items found...

Get smarter in engineering and leadership in less than 60 seconds.

Join 300+ founders and engineering leaders, and get a weekly newsletter that takes our CEO 5-6 hours to prepare.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.