Step 6: backend

In this Pull Request, we are adding a backend to our project. We will be using PosgreSQL database running in a Docker container in our local development environment. We will also upgrade Angular to version 8, just because it came out recently and there’s no reason not to keep our dependencies fresh.

We will begin with just a single table in our database called users, see db.ts:

We will list all our tables in an enum, this always helps with keeping track of lists – hard-coding may feel like it’s faster, but enums help with maintainability a lot.

We are connecting to our database using knex, the connection string is reused from the knexfile:

Knex handles migrations and seeds for us, these sit in knex/ folder along with the knexfile.

Migration is a function that modifies the database, usually database structure. Our first migration will create the users table, see 191015-users.ts:

Migration can be applied or rolled back; when it’s applied, up callback is executed, rollback is done by the down callback. We are creating the table to store our users hashed passwords, emails, names, and preferred languages, along with created and updated timestamps.

We’ll also seed our database with one test user, see test-user.ts:

Knex handles these files automatically, we just need to add shortcuts to our package.json:

Note how npm run db:migrate will now apply our migrations and npm run db:seed will run our seed files. Let’s see how the database is created with npm run db:create – this is based on Docker, we docker-compose our dev environment using docker-compose.db.yml:

Here, we create 2 containers: one for PostgreSQL itself, which will run on port 5432, and one more for pgAdmin4 – a nice database GUI tool to help us debug, it will run on port 8432.

This will require a local installation of Docker, and having Portainer installed is also highly recommended. When you run npm run db:create, you should be able to see 2 new containers called backend-db and pgadmin in your portainer:

Run npm run db:migrate, npm run db:seed and open http://localhost:8432/ to access your local database in pgAdmin4 – you should see the test user in users table:

This is quite amazing – we have multiple containers running on our development machine, it feels the same as a cluster of servers somewhere in AWS cloud. All developers get the same versions and configurations. Docker is so cool! ๐Ÿ™‚ Note how we prepare pgAdmin4 installation with a list of servers and a password file so you can connect to your local database without having to enter connection info manually. The user name is “postgres” and the password is “password” – the defaults are good enough for local development.

You will note that we added a new application to our angular.json:

This new app called backend is our API. It’s based on Node Express server and is fairly simple. The app is bootstrapped in main.ts:

Note that we have to use cors here, because our backend sits on a different domain than the frontend, even locally – the frontend is at http://localhost:4200 while the backend is running at http://localhost:3000, so we have to use cors, otherwise the requests get blocked by the browser.

For authentication, we attachAuthStrategies found in attach-auth-strategies.ts:

We use a popular library called Passport for authentication, specifically the Bearer strategy. What this does is it uses a JWT token with a signed user ID – the token is signed with a secret available only on the server side and has an expiration date of 2 weeks. This happens in users.ts:

The token is created in makeAuthToken function using jwt.sign method.

Note how we approach the database structure here – one file per database table. Firstly, we expose the interface User that defines the shape of a single record in the users table, and secondly we expose the database operations grouped simply as static methods of the Users class. This encapsulates everything we need to do with our users table on the level of database.

Our first actual interaction with the database begins upon user registration. This is handled in /log-in API endpoint handler, see auth.ts:

This API endpoint handles both logging in of existing users and registering new ones. If user with the specified email is not found in the database, we insert a new record using Users.create() and send en email with the activation link – the user is created with email field set to null, but unconfirmedEmail is where the data provided by the user is stored.

Email is sent using sendMail function found in send-email.ts:

We pick a Handlebars template from the assets directory based on the user’s language, inject it with the data provided and send the result using nodemailer. Note how we use environment variables to create the email transport – if MAIL_HOST is empty, we’ll just print the emails to console for debugging purposes.

The backend environment variables are all bootstrapped in environment.ts:

This is what the email layout looks like in layout.hbs:

The variable body is where the actual body of the email is inserted, it’s generated from another Handlebars template found in ACTIVATION/en/body.hbs:

It looks pretty good in email client, too:

Note the directory structure – each email template has a folder with a subfolder per language; each language directory has 2 files: body.hbs and subject.hbs, both are compiled using Handlebars.compile and then injected with the data, in this case with the activateEmailUrl. The data is strictly typed. Take a look at this piece from the /log-in route handler in auth.ts once again, it will probably make more sense now:

The activation link goes to one of the 2 new frontend pages we added in this PR, see app-routing.module.ts:

The activate email page component immediately hits the backend API to activate the user, sending back the authToken from the route params, see activate-email-page.component.ts:

We take the email and authToken from the route parameters and hit activateEmail API – if everything goes well, the user is activated on the backend and logged in on the frontend – we redirect the user to their profile page immediately upon success. If something goes wrong, we show an error message. This is how the template looks in activate-email-page.component.html:

Nothing fancy here, we don’t expect our users to spend a lot of time on this page ๐Ÿ™‚

This API request is handled on the server side by another route handler in auth.ts:

We use jwt.verify to check the authToken from the request, and if it matches the email, we activate the user by moving the email from unconfirmedEmail field to email field. Note how we return user preferred language in the API response here – if the new user switches to Russian and registers, the user record will hold the information about language preference in the database. Now, this same user could click on the activation link from another device – but still they will be switched to Russian immediately upon activation. This will also work for every log in – when user logs in, the API response contains their preferred language from the database record, and the UI will switch to the user’s preferred language immediately. This is ensured by a new line we added to AuthService.logIn in auth.service.ts:

Reset Password email functionality is more or less identical to the Activation email – we’ll get back to this feature in the upcoming PR, currently the email is implemented, but there’s no form on the frontend to actually reset the password, it’s just a little stub in reset-password-page.component.ts:

All backend API endpoints are listed in ApiUrls enum in api-urls.enum.ts:

Both /user and /me API endpoints serve user data, the difference is how they are setup – /me requires the user to be logged in, while /user is a public API. In fact, /me is the first non-public API we introduce. The frontend sends authentication data to the backend in a special HTTP header Authorization (that is then handled by Passport on the server side), but only for non-public API endpoints, see api.service.ts:

The routes are attached to the backend app main Express router in a particular order – the protected routes that require user to be authenticated are listed only after the Passport authentication middleware is attached in router.ts:

Passport puts authenticated user id into request.user field in protected routes, so we can easily access it in the route handler, see own-user-record.ts:

Conclusion

I hope you enjoyed this tutorial. We have a serious RDBMS in our dev stack now, multi-lingual template-based emails – our project begins to feel like an app that could actually serve people. Do not hesitate to contact me with any questions or feedback. We have a lot more ground to cover… onward! ๐Ÿ™‚

Cheers,
Andrew