Implementing Basic Authentication using Express and Typescript

ganesh mani
9 min readJun 2, 2021

Do you use express for your backend and curious about building a basic auth using express and Typescript or exploring to adapt Typescript for your application backend? Then, you’re in the right place. I often see most of the tutorial recommends frameworks like NestJS. But, An important thing to keep in mind is to learn the basics and structure before jumping into the framework. So, we are going to build express typescript basic auth

Here, we will start with the essential Typescript concepts and patterns to build our backend system. Of course, it can be opinionated too. But let’s compare them and understand when we need those concepts.

Before getting into the content, the whole tutorial is split into series to explain it in detail.

  1. Project Structure, Database Connection, and Error Handling — Part 1
  2. Authentication System using Express and Typescript — Part 2

Setting up Express and Typescript is simple and straightforward. There are a lot of articles and documentation are available for initial setup. Check this out.

Project Structure

Once you set up the basic skeleton for the application. It’s time to understand the project structure. Here, we are going to follow a modular system.

Let’s say that our application has User and project API. So we are going to create a User and Project directory and group them based on it.

We can group all the helpers and common functionalities under the Common directory. It helps us to separate the logic, which comes in handy when the application grows.

Here’s the finalized version of our project structure.

Each module will contain different files that perform functionality. They are,

Route Configuration — It handles all the routes for the particular module. For example, if we have a user module. It handles all the GET, POST, PUT for the user module.

Controller — It manages the business logic for each endpoint. It can be fetching the users, updating the user details, etc.

Services — It handles the connection with database models and performing queries, and return that data to Controller.

Model — Here, we define our mongoose schema, which is an ODM for MongoDB. We can also use sequelize to handle Postgres, MySQL.

Interface — it helps us to define custom data types.

Route Configuration

We can configure routes in many different ways. However, since we use Express and Typescript, we will go with Typescript concepts for a more structural codebase.

Every module will have routes for its functionality. So, the structure will be the same for all the modules. To restrict the same standard across our codebase, we will use an abstraction pattern with Typescript.

Implement an abstract class with an abstract method for route configuration,

1import express, { Application } from “express”

3export abstract class RouteConfig {

6 constructor(app: Application, name: string) {

16 abstract configureRoutes(): Application

Here, we have properties such as app , name, and an abstract method.

app - it contains the Express Application Instance

name - it defined the route config name. For example, User Route Config.

configureRoutes - this is where all the classes will implement the router for a module.

Every class that inherits this abstract class must implement configureRoutes method. In that way, we will know it follows a standard.

Let’s create a route config for the User module,

1import { RouteConfig } from “../Common/common.route.config”

2import express, { Application, Request, Response } from “express”

3import UserController from “./user.controller”

5export class UserRoutes extends RouteConfig {

6 constructor(app: Application) {

11 this.app.route(`/users`).get([UserController.getUsers])

An important thing to note here is configureRoutes. Here, we have our route implementation. it may be different for developers who used app.get() and app.post(). But, We use app.route().

It provides an instance of a single route with which we can handle HTTP verbs with middleware. It mainly helps to group the route HTTP’s based on the name. For example, we can group all the GET, POST, PUT requests of route users. We can also pass middleware to that particular route.

We define GET request for route,

1this.app.route(`/users`).get([UserController.getUsers])

Controller for the User is simple and straight forward. It will be something like,

1import { Request, Response, NextFunction } from “express”

5 async getUsers(req: any, res: Response, next: NextFunction) {

6 return res.status(200).json({

20export default new UserController()

Once you create routes and Controllers, you can add them in index.ts. Next, we instantiate each module route and serve them in the entry file.

1import { RouteConfig } from “./Common/common.route.config”

2import { UserRoutes } from “./User/user.route.config”

4const routes: Array = []

6routes.push(new UserRoutes(app))

index.ts after the changes,

1import express, { Express, Application, Request, Response } from “express”

4import dotenv from “dotenv”

5import { RouteConfig } from “./Common/common.route.config”

6import { UserRoutes } from “./User/user.route.config”

7const routes: Array = []

9const app: Express = express()

13app.use(express.json())

16const PORT = process.env.PORT || 8000

19 process.on(“unhandledRejection”, function(reason) {

25routes.push(new UserRoutes(app))

27app.get(“/”, (req: Request, res: Response) => {

28 res.send(“Welcome world”)

31const server: http.Server = http.createServer(app)

32server.listen(PORT, () => {

33 console.log(`Server is running on ${PORT}`)

35 routes.forEach((route: RouteConfig) => {

36 console.log(`Routes configured for ${route.getName()}`)

So far, we have a basic setup for route and Controller. If you run the application, you can see the application running,

Database Connection

Before implementing the connection to the database, we need to install the required dependencies for it.

2npm i — save-dev @types/mongoose @types/debug

We all know the use of Mongoose. If you’re new to Mongoose, you can learn the basics here. We also install debug package. It helps us to log all the debug in the console. It's better than console.log

Let’s connect MongoDB with the application. Create directory services and mongoose.services.ts and add the following code,

1import mongoose from “mongoose”

2import debug, { IDebugger } from “debug”

4const log: IDebugger = debug(“app:mongoose-service”)

8 private mongooseOptions = {

10 useUnifiedTopology: true,

12 serverSelectionTimeoutMS: 5000,

13 useFindAndModify: false,

17 this.connectWithRetry()

25 log(“process.env.MONGODB_URI”, process.env.MONGODB_URI)

26 const MONGODB_URI = process.env.MONGODB_URI || “”

27 log(“Connecting to MongoDB(Retry when failed)”)

29 .connect(MONGODB_URI, this.mongooseOptions)

31 log(“MongoDB is connected”)

36 `MongoDB connection unsuccessful (will retry #${++this

37 .count} after ${retrySeconds} seconds):`,

40 setTimeout(this.connectWithRetry, retrySeconds * 1000)

45export default new MongooseService()

connectWithRetry is the main function that connects our application to MongoDB. It also retries the connection after 5 seconds of the failure.

We get an instance of Mongoose using the getInstance method to have a single instance across the application.

So far, we have seen Project Structure, Route Configuration, and Database Connection. It’s time to implement Authentication for the application. For Authentication, we need

  • Login.
  • Signup.
  • User route to get the logged-in user.

Since Authentication is a workflow itself, we can create a separate module and write logic there. So here’s the flow to build Authentication.

  • Create User Model — It creates a model with which we can connect with MongoDB users collection.
  • Implement Authentication Route Configuration — route configuration for Auth. i.e., login and signup
  • Create Authentication Controller — Controller interacts with services for data fetch and update.
  • Build Authentication Service — Service interacts with DB for database operations.

Create user.model.ts and add the following code,

1import MongooseService from “../Common/services/mongoose.service”

2import { model, Schema, Model, Document } from “mongoose”

3import { scrypt, randomBytes } from “crypto”

4import { promisify } from “util”

5import { IUser } from “./user.interface”

6import { Password } from “../Common/services/password”

7const scryptAsync = promisify(scrypt)

8export interface UserDocument extends Document {

14interface UserModel extends Model {

15 build(attrs: IUser): UserDocument

18const UserSchema: Schema = new Schema(

20 email: { type: String, required: true },

21 password: { type: String, required: true },

22 username: { type: String, required: true },

26 transform: function(doc, ret) {},

29 transform: function(doc, ret) {

36UserSchema.pre(“save”, async function(done) {

37 if (this.isModified(“password”)) {

38 const hashed = await Password.toHash(this.get(“password”))

39 this.set(“password”, hashed)

44UserSchema.statics.build = (attrs: IUser) => {

48const User = MongooseService.getInstance().model(

It may be overwhelming and confusing at the beginning. But let me break it down and explain everything.

First and foremost, Every Mongoose model starts with a schema. So, we define a Schema here.

1const UserSchema: Schema = new Schema(

3 email: { type: String, required: true },

4 password: { type: String, required: true },

5 username: { type: String, required: true },

9 transform: function(doc, ret) {},

12 transform: function(doc, ret) {

Every time we send the user data in response. We don’t want to expose the password in response. So to remove it, we implement this toJSON here.

3 transform: function (doc, ret) {},

6 transform: function (doc, ret) {

While users signup, we need to insert that data into DB. In Mongoose, we can implement it in two ways.

  1. Using .create method
  2. Using new keyword to create an object. For example, new User().

We are going to use a new User to create a user here. But the problem with that approach is, Typescript doesn't check the type when we create a new User(). So it doesn't complain even if we change the argument name in it.

It completely affects the purpose of using Typescript in the application. To fix it, we somehow need to create a method that checks the type before passing it to a new User().

create an interface user.interface.ts and add the following code

1UserSchema.statics.build = (attrs: IUser) => {

So, we create a build method that takes the type of IUser, which is an interface here, and pass it to a new User()

If we try to use User.build in our Controller. Typescript will complain something like this,

Even though we created a custom method in the Schema, the model Type in Typescript doesn’t know. So, we need to extend Mongoose Model and add this custom method to it.

1export interface UserDocument extends Document {

7interface UserModel extends Model {

8 build(attrs: IUser): UserDocument

That completes the User Model. let’s create Auth route, Controller and services.

auth.route.config

1import { Application, Request, Response } from “express”

2import { RouteConfig } from “../Common/common.route.config”

3import AuthController from “./auth.controller”

4export class AuthRoutes extends RouteConfig {

5 constructor(app: Application) {

10 this.app.route(“/login”).post(AuthController.login)

12 this.app.route(“/signup”).post(AuthController.signup)

auth.controller.ts

1import { NextFunction, Request, Response } from “express”

2import AuthService from “./auth.service”

3import jwt from “jsonwebtoken”

4import debug, { IDebugger } from “debug”

5import { Password } from “../Common/services/password”

6const jwtSecret: string = process.env.JWT_SECRET || “123456”

7const tokenExpirationInSeconds = 36000

9const log: IDebugger = debug(“auth:controller”)

14 async login(req: Request, res: Response, next: NextFunction) {

16 const email = req.body.email

17 const password = req.body.password

19 const user = await AuthService.findUserByEmail(email)

22 const isPasswordMatch = await Password.compare(user.password, password)

25 throw new Error(“Invalid Password”)

27 log(“jwt Secret”, jwtSecret)

28 const token = jwt.sign(req.body, jwtSecret, {

29 expiresIn: tokenExpirationInSeconds,

32 return res.status(200).json({

40 throw new Error(“User Not Found”)

47 async signup(req: Request, res: Response, next: NextFunction) {

49 const username = req.body.username

50 const email = req.body.email

51 const password = req.body.password

53 const user = await AuthService.findUserByEmail(email)

56 throw new Error(“User Already Exists”)

59 const newUser = await AuthService.createUser({

65 const token = jwt.sign({ username, password }, jwtSecret, {

66 expiresIn: tokenExpirationInSeconds,

69 return res.status(200).json({

75 log(“Controller capturing error”, e)

76 throw new Error(“Error while register”)

85export default new AuthController()

auth.service.ts

1import User from “../User/user.model”

2import { IUser } from “../User/user.interface”

4 async createUser(data: IUser) {

6 const user = User.build(data)

13 async findUserByEmail(email: string) {

20export default new AuthService()

Middleware

To validate the user based on the jwt token, create a middleware JWT.ts and add the following code,

1import jwt from “jsonwebtoken”

2import { Request, Response, NextFunction } from “express”

3const JWT_KEY = process.env.JWT_SECRET || “123456”

4import debug, { IDebugger } from “debug”

6const log: IDebugger = debug(“middleware:JWT”)

9 authenticateJWT(req: Request, res: Response, next: NextFunction) {

10 const authHeader = req.headers.authorization

11 if (authHeader && authHeader !== “null”) {

13 log(“auth Header”, JWT_KEY)

14 jwt.verify(authHeader, JWT_KEY, (err: any, user: any) => {

19 .send({ success: false, message: “Token Expired” })

25 res.status(403).json({ success: false, message: “UnAuthorized” })

We get the token from the header and verify it with just. We can use the middleware in the route like,

1this.app.route(`/user`).get([JWT.authenticateJWT, UserController.getUser])

Complete source code is available here

Want to stand out from the Crowd?

Don’t get stuck in the tutorial loop. Learn a technology by practicing real world scenarios and get a job like a boss. Subscribe and get the real world problem scenarios in your inbox for free

No spam, ever. Unsubscribe anytime.

--

--