With the AI service completed, we now focus on setting up the base backend service. As outlined in the system's architecture, our main backend will be powered by Node.js with Express.js, using MongoDB as the database.

While we could have chosen other combinations—such as Node.js with PostgreSQL, Bun with PostgreSQL, or Django/Flask/FastAPI/aiohttp with SQL/NoSQL/NewSQL—I opted for this stack as a refresher.

We'll begin developing the backend service here and continue refining it in subsequent articles. Let's dive in! 🚀

We assume you have already set up a TypeScript-based Express project. If not, follow these simple steps:

  • Create a new folder, say backend, and change the directory into it

  • Run npm init -y to initialize a node project:

    sh

    This will create a normal package.json file with very basic entries.

  • Install TypeScript and create a tsconfig.json file

    sh

At this point, you have a minimal Node.js app with TypeScript support. However, for our project, we need a more structured setup. Modify your package.json as follows:

package.json
diff

We want to use ESM instead of CommonJS hence the "type": "module". We also want to use the latest node.js LTS (v22 at the time of writing). We also want some structure where all source files are in the src directory and tests in the tests directory with the entry point, during development, as src/app.ts (in production, it'll be dist/app.js). Next, make tsconfig.json look like this:

tsconfig.json
json

You can get a better explanation for each of the entries tsconfig reference. The idea is we want to use the latest entries while being considerate. We also created aliases (that's what paths does) so that instead of doing ../../../src/config/base.js, we would just do: $config/base.js. Nifty stuff!

Sirneij
Sirneij/finance-analyzer
00

An AI-powered financial behavior analyzer and advisor written in Python (aiohttp) and TypeScript (ExpressJS & SvelteKit with Svelte 5)

sveltetypescriptpythonjavascriptcss3html5

Now it's time to get our hands dirty. We will be implementing the OAuth-based authentication system here.

We will use passports.js and passport-github2 for implementing the authentication strategy. Let's install the libraries (and types to keep TypeScript happy):

sh
Note: Create a GitHub OAuth App

As with most OAuth services, you need to create a new GitHub OAuth app to use its authentication strategy. After creation, you will be provided with GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET which are required by passport-github2. Ensure you fill in your app details correctly.

Having installed the packages, let's start with some configurations. Create src/types/misc.types.ts and populate it with:

src/types/misc.types.ts
ts

Though we are only supporting GitHub OAuth for now, we defined an enum that includes Google as well. The BaseConfig interface will be used for all the app's configurations including auth and db which have standalone types:

src/types/auth.types.ts
ts
src/types/db.types.ts
ts

As previously stated, we need each OAuth service's cliend_id and client_secret to authenticate. Another important credential is the callback_url (also called redirect URI in some OAuth implementations) which is the URL where the OAuth service redirects users after authentication. You will supply this at the point of registering or creating a new OAuth app in GitHub (and other providers as well).

Next, let's find a way to populate these credentials. We will use the dotenv package to retrieve them from a .env file or environment variables:

src/config/base.config.ts
ts

We use getter methods in our configuration for three key benefits:

  1. Dynamic Values: Getters retrieve values on-demand, ensuring we always get the latest values
  2. Environment Variables: Particularly important for process.env values that may change during runtime
  3. Lazy Evaluation: Values are only computed when accessed, improving performance

I personally encountered a bug in production where my authentication process was failing because stale values were being read by process.env for frontendUrl.

Here are the contents of src/config/internal/auth.config.ts:

src/config/internal/auth.config.ts
ts
src/config/internal/db.config.ts
ts
src/config/internal/logger.config.ts
ts
Tip: Use MongoDB Atlas

You probably have MongoDB installed on your machine and you can just use it for development. In case you need to test in production, check up the free version of the MongoDB Atlas.

With the configurations underway, let's create services that will connect our application to MongoDB as well as redis (for session storage).

Note: Why Redis for Session Storage?

While express-session offers in-memory storage, Redis is preferred for production because:

  • Persistence: Sessions survive server restarts
  • Scalability: Handles high traffic and multiple server instances
  • Performance: Fast read/write operations with minimal latency
  • Memory Management: Automatic memory optimization and key expiration

Using in-memory storage in production can lead to:

  • Lost sessions after server restarts
  • Memory leaks as sessions accumulate
  • Scaling issues with multiple server instances

Let's create a database service for them:

src/services/db.service.ts
ts

For the MongoDB connection, we implemented a retry logic in case some connection attempts fail. Aside from that, it's a basic way to connect to a MongoDB instance. We did something equivalent to redis. Now, we can proceed to hook all these up in src/app.ts.

Let's populate our src/app.ts with the following:

src/app.ts
ts

It is a simple setup that also considers production environments. We started by creating an express application instance which is needed to attach. Next, we enabled the "trust proxy" which allows some features for applications behind a proxy. Then we informed Express to parse incoming requests with JSON payloads by "using" the express.json() middleware. This is a way of attaching middleware to express. We also used the CORS and session middlewares to appropriately configure our application's CORS for inter-origin resource sharing and sessions. Specifically, we are giving the backend a "go-ahead" to share resources with our front end. We also made sure our sessions were secured by providing them with an option to use our generated secret keys. In development, you can use openssl to generate a 32-bit secret key:

sh

In production, you can opt for Cryptographically generated bytes.

After that, we used passport's authentication middleware and extended its feature to easily serialize and deserialize our app's User object. In the spirit of authentication, we defined our GitHub OAuth strategy next and it follows the normal anatomy of OAuth strategies supported by passport and specifically, passport-github2. Because we defined our credentials perfectly, we just passed it in, else we would have done something like:

ts

In the callback function, we have access to the profile data returned by GitHub which was then passed into the AuthService to create the user in the database:

src/services/auth.service.ts
ts

We defined a user model (with its schema) already:

src/models/user.model.ts
ts

The types used so far for the user can be found in src/types/auth.types.ts:

src/types/auth.types.ts
ts

To make TypeScript happy with our custom user type, we needed to modify its user type in src/types/passport.d.ts:

src/types/passport.d.ts
ts

The rest of the src/app.ts are pretty basic. Before we wrap up with this article, let's see what the authentication routes are.

In src/app.ts, we used:

src/app.ts
ts

These routes are in src/routes/auth.routes.ts:

src/routes/auth.routes.ts
ts

The first one is where the authentication flow starts. It lets you login into your GitHub account and if successful redirects you to the callback_url the developer supplied during OAuth app creation, for us, it's the second route.

Tip: Supplying redirect route in the frontend

Let's say a user wants to access /private/route in your app's front end but such a user wasn't authenticated. Then your frontend app redirects the user to login with GitHub (and provides a next=/private/route in the URL). What the user expects is after a successful login, they want to be sent back to where they were headed initially /private/route. That was the logic implemented in the /github route above. It simply "remembers" the user's previous state.

These routes are very basic. We won't talk much about them. However, they used some "controllers" which we haven't seen yet:

src/controllers/auth.controller.ts
ts

We redirected responses back to our frontend app.

With that, I will say see you in the next release! Check out the GitHub repository for the other missing pieces.

Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and X. I am also an email away.

If you found this article valuable, consider sharing it with your network to help spread the knowledge!