Design Pattern: Node.js with Typescript.
By: Jcsouz Ibm
Tags: Design Patterns, Good Practices, IBM, Nodejs, Node Js Developer

Short Intro

Hi guys, my name is Jean, I’m Software Developer at IBM and I gonna show how my team and I work with Node.js and our good practices in the back-end…

1 — Project Structure:

Currently, we split our files into 4 folders, in all of those folders we have files that we separate by priority and we can re-use any file to make a new feature.

1.1 — Controller:

In this folder, we have a file with functions that we need to process the data that we are receiving and to return to the client. Here we pass the data that we are receiving processed to his respective model.

An example is:

We have a notification feature, so we have a file called notification.ts and in this file, we have two functions, the first is getNotifications and another is createNotification , I will use the getNotificaftions to show an example:

export const getNotifications = async (req: Request, res: Response) => { try { const userEmail: string = req.params.emailLoggedUser; if (!userEmail) { res.status(404).send('Missing logged user email.'); return false; } const notificationList = await queryGetNotifications(userEmail); res.status(200).send(notificationList); return true; } catch (err) { res.status(400).send(err); return false; } };

In this example, we are receiving the params called emailLoggedUser after that, we get this params and we make verification to see if the params exist really.

const userEmail: string = req.params.emailLoggedUser; if (!userEmail) { res.status(404).send('Missing logged user email.'); return false; }

If there is no params we return to the client with a message of error and his status.

Bellow this verification, we have a called to his respective model, in this case, we call as queryGetNotifications and we save the result in a variable to send to the client.

1.2 — Model:

The Model folder follows the same Controller rule, but in the model files, we have the database connection and business rule.

Following the example:

export const queryGetNotifications = async (loggedUserEmail: string): Promise<INotificationProcessed> => { const query = { name: 'get_notifications', text: `SELECT message FROM notifications AND user_logged_email = $1 ORDER BY date_event`, values: [loggedUserEmail], };

try { const response = await pool.query(query); if (response.rowCount > 0) { const messages: INotifications[] = response.rows; const countMessages: number = messages.length; const notifications = { messages, count_messages: countMessages, }; return notifications; } return { messages: [], count_messages: 0, }; } catch (err) { return err; } };

Here, we are receiving the params from controller < getNotifications >, and we use it in our query .

Here, we wait for the result with some proprieties (in this case, we are using a library to make our life easy) from the query to make verification:

const response = await pool.query(query); if (response.rowCount > 0) { const messages: INotifications[] = response.rows; const countMessages: number = messages.length; const notifications = { messages, count_messages: countMessages, }; return notifications; } return { messages: [], count_messages: 0,

};

If rowCount (propriety from the library) is greater than zero we can process the data and send it to controller but otherwise if rowCount is equal 0 we return to controller an object without data.

And the controller returns this data to the client.

1.3 — Test:

All of these files, both inside Controller and Model we need to make sure that they are working on how we are expecting, for this, we make tests for all files. To make that we use jest .

In this folder we separate some subfolders like this:

1.3.1 — Integration:

Usually we put only controller files because here we test also model files .

1.3.2 — Unit:

Here we put the model files because we can make a test for each function in isolation.

Example of test (Unit):

describe('queryGetNotifications', () => { it('Test query', async () => { const spy = jest.spyOn(pool, 'query'); const email = 'jean@ibm.com';

const query = { name: 'get_notifications', text: `SELECT id,event_message FROM notifications WHERE date_event >= NOW() - interval '1 month' AND user_logged_email = $1 ORDER BY date_event`, values: [email], }; await queryGetNotifications(email); expect(spy).toBeCalledWith(query); });

In this test above, we are testing only the query and if anyone changes the query in the model we will know because when we run the tests this specific test will fail and this test ensures that everything it’s running as expected.

1.4 — Migration:

For last but very important migration folder, to manage our database we use a library called db-migrate, with it we can create tables, create views, populate tables, make changes in specific tables or view, and everything is documented, and registered when we did any change on the database.

2 — Typescript:

One of the points of the Typescript is to allow us to find errors during development, another one is to allow our IDE to know the format of the params, function returns, variable and etc..

In our case, the folders controllers and models we have a file called types , in this file, we have all types for specific file and function.

Following the example using feature Notification , we have file type in model folder :

export interface INotifications { event_message: string; id: number; owner_notification: string; } export interface INotificationProcessed { messages: INotifications[]; count_messages: number; }

With this interface, we can type the function return, the format of a variable, in for example, in our model:

if (response.rowCount > 0) { const messages: INotifications[] = response.rows; const countMessages: number = messages.length; const notifications = { messages, count_messages: countMessages, }; return notifications; }

You can see that countMessages also is typed as number , if messages.length is anything different than a number, it will break and we will know.

In our controller we have another example:

const userEmail: string = req.params.emailLoggedUser;

If req.params.emailLoggedUser is anything different than string it will break too.

In a model function , for example, we have an argument typed as a string:

export const queryGetNotifications = async (loggedUserEmail: string): Promise<INotificationProcessed> => { return { messages: [], count_messages: 0, }; };

If you called this function in the controller passing a number, for example, at the time it will say that you need to pass a string as a parameter. You can see also that we are typing the return of the queryGetNotifications as INotificationProcessed if you change messages to hello for example, you will receive this message:

Object literal may only specify known properties, and ‘hello’ does not exist in type ‘INotificationProcessed’.

and with that we can guarantee that everything that all the values ​​that the client needs are correct and if someone changes in the future, the typescript will notify.

I think that is guys, I will do another post here about db-migrate , jest and node-postgres soon.

Regards!