Authentication Service in TypeScript and Clean Architecture

Table of Contents

This guide will walk you through the process of creating an authentication service in Node.js using TypeScript and clean architecture. The authentication service will allow users to sign up and log in to your application using email and password.

Prerequisites

Before you begin, make sure you have the following:

Project Setup

To set up the project, follow these steps:

  1. Create a new directory for your project.

  2. Open the directory in your terminal or command prompt.

  3. Initialize a new Node.js project using npm init or yarn init.

  4. Install the required dependencies:

    • express for building the server
    • jsonwebtoken for generating and verifying JSON Web Tokens
    • bcrypt for hashing and verifying passwords
    • dotenv for managing environment variables
    • knex.js for database management
    • pg or any other driver for the desired database engine
    • @types/express, @types/jsonwebtoken, @types/bcrypt, @types/dotenv and @types/pg for TypeScript support

    You can install these dependencies using the following command:

    npm install express jsonwebtoken bcrypt dotenv typeorm reflect-metadata pg @types/express @types/jsonwebtoken @types/bcrypt @types/dotenv @types/pg --save
    
  5. Create a src directory for your source code.

  6. Inside the src directory, create a server.ts file for your server code.

Clean Architecture Setup

We will be using the Clean Architecture pattern to structure our code. The Clean Architecture is a software architecture that separates the code into layers with clear boundaries and dependencies pointing inwards. In this architecture, the core business logic is at the center of the architecture, surrounded by layers of less important details. The layers are:

The following diagram illustrates the Clean Architecture layers:

+----------------+
|   Controllers  |
+----------------+
|     Use Cases  |
+----------------+
|    Entities    |
+----------------+
|  Repositories  |
+----------------+
|    Services    |
+----------------+

To set up the Clean Architecture, create the following directories inside the src directory:

Server Setup

Inside the server.ts file, you will need to create a new Express.js server, set up middleware for parsing JSON and handling CORS, and define routes for authentication. Here is an example of what your server.ts file might look like:

import express from "express";
import bodyParser from "body-parser";

class App {
  private readonly app: express.Application;

  constructor() {
    this.app = express();
    this.config();
    this.registerRoutes();
    this.startServer();
  }
  private config() {
    this.app.use(bodyParser.urlencoded({ extended: false }));
    this.app.use(bodyParser.json());
  }


  // Initialize all the routes of the application
  private registerRoutes(): void {
    const router = express.Router();

  }

  // Server will listen to this port
  private startServer() {
    try {
      const PORT: number = 8080;
      this.app.listen(process.env.PORT || PORT, () => {
        console.log(`App listening on port ===> http://localhost:${PORT}/`);
      });
    } catch (error) {
      console.error('Server could not be started', error);
      process.exit(1);
    }

  }
}



export default App;

Entites

Entities are at the heart of the domain layer. They are the core objects that represent the business concepts of the application. Entities contain data and methods that are specific to the business domain and are independent of any particular application logic or technical implementation.

This interface defines the properties of a User entity, including its unique identifier, first and last name, email address, password, and timestamp for when it was created and last updated. The properties can be read or modified through methods defined in the domain layer.

To define an entity in TypeScript, you can use an interface or a class. Here’s an example of an interface for a User entity:

export interface User {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
}

Repository

The Repository layer is responsible for managing the persistence of entities. In the context of our authentication service, the repository will handle operations related to storing and retrieving user data from the database.

To create the UserRepository, follow these steps:

  1. Inside the src directory, create a new directory called repositories.

  2. Inside the repositories directory, create a file called UserRepository.ts.

  3. In the UserRepository.ts file, import the necessary dependencies:

    import { getRepository, Repository } from 'typeorm';
    import { User } from '../domain/entities/User';
    
  4. Define a class called UserRepository and export it:

    export class UserRepository {
      private repository: Repository<User>;
    
      constructor() {
        this.repository = getRepository(User);
      }
    }
    
  5. Add methods to the UserRepository class for handling database operations. For example, you can add methods for creating a new user, finding a user by email, or updating a user’s password.

    Here’s an example of a method for creating a new user:

    async createUser(user: User): Promise<User> {
      const newUser = await this.repository.create(user);
      return this.repository.save(newUser);
    }
    

    Note that the getRepository function is provided by TypeORM, which is a popular Object-Relational Mapping (ORM) library for TypeScript and JavaScript.

  6. Save the file and move on to the next step.

Use Cases

The Use Case layer contains application-specific business rules and orchestrates the flow of data between the entities and the repositories. In the context of our authentication service, the use cases will handle the logic for signing up and logging in users.

To create the Signup use case, follow these steps:

  1. Inside the src/usecases directory, create a file called Signup.ts.

  2. In the Signup.ts file, import the necessary dependencies:

    import { UserRepository } from '../repositories/UserRepository';
    import { User } from '../domain/entities/User';
    import { hashPassword } from '../utils/passwordUtils';
    
  3. Define a class called Signup and export it:

    export class Signup {
      private userRepository: UserRepository;
    
      constructor(userRepository: UserRepository) {
        this.userRepository = userRepository;
      }
    }
    
  4. Add a execute method to the Signup class that takes in the necessary input data for signing up a user, such as email and password:

    async execute(email: string, password: string): Promise<User> {
      // Hash the password
      const hashedPassword = await hashPassword(password);
    
      // Create a new user entity
      const newUser: User = {
        id: '', // Generate a unique identifier
        email,
        password: hashedPassword,
        firstName: '',
        lastName: '',
        createdAt: new Date(),
        updatedAt: new Date(),
      };
    
      // Save the new user to the database
      return this.userRepository.createUser(newUser);
    }
    

    In this example, the hashPassword function is a utility function that hashes the user’s password before storing it in the database. You can implement this function using a library like bcrypt.

  5. Save the file and move on to the next step.

Controllers and Routes

The Controllers layer handles incoming requests, validates input data, and interacts with the use cases to process the request. In the context of our authentication service, the controllers will handle the HTTP endpoints for signing up and logging in users.

To create the AuthController and define the authentication routes, follow these steps:

  1. Inside the src/interface directory, create a file called AuthController.ts.

  2. In the AuthController.ts file, import the necessary dependencies:

    import { Request, Response } from 'express';
    import { Signup } from '../usecases/Signup';
    import { UserRepository } from '../repositories/UserRepository';
    
  3. Define a class called AuthController and export it:

    export class AuthController {
      private signup: Signup;
    
      constructor() {
        this.signup = new Signup(new UserRepository());
      }
    }
    
  4. Add methods to the AuthController class for handling the signup and login endpoints. For example, you can add a method for handling the POST request to /signup:

    async signupHandler(req: Request, res: Response): Promise<void> {
      const { email, password } = req.body;
    
      try {
        const user = await this.signup.execute(email, password);
        res.status(201).json(user);
      } catch (error) {
        res.status(500).json({ error: 'Failed to create user' });
      }
    }
    

    Note that in this example, the signupHandler method calls the execute method of the Signup use case to create a new user. If the signup is successful, it responds with the created user; otherwise, it returns a 500 error.

  5. Define the routes for authentication in the registerRoutes method of the App class in the server.ts file. For example, you can add the following route for the signup endpoint:

    router.post('/signup', authController.signupHandler.bind(authController));
    

    Here, authController.signupHandler is bound to the route handler to maintain the correct context when handling the request.

  6. Save all the files and start the server using npm start or yarn start. You should now be able to send HTTP requests to the authentication endpoints.

Congratulations! You have successfully set up an authentication service in Node.js using TypeScript and clean architecture. You have learned how to structure your code using the Clean Architecture pattern, set up the server, define entities and repositories, create use cases, and handle HTTP requests with controllers and routes. From here, you can further enhance your authentication service by adding features like login, password reset, and email verification.

Remember to always follow security best practices when implementing authentication, such as using strong password hashing algorithms, protecting against brute force attacks, and handling session management securely.

Github repo link

Read Next

A Distributed in-memory storage Engine in Rust

Go to top