Authentication Service in TypeScript and Clean Architecture
Table of Contents
- Prerequisites
- Project Setup
- Clean Architecture Setup
- Server Setup
- Entites
- Repository
- Use Cases
- Controllers and Routes
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:
- Node.js (v14 or higher)
- npm or yarn
- A text editor of your choice (e.g. VS Code)
Project Setup
To set up the project, follow these steps:
Create a new directory for your project.
Open the directory in your terminal or command prompt.
Initialize a new Node.js project using
npm initoryarn init.Install the required dependencies:
expressfor building the serverjsonwebtokenfor generating and verifying JSON Web Tokensbcryptfor hashing and verifying passwordsdotenvfor managing environment variablesknex.jsfor database managementpgor any other driver for the desired database engine@types/express,@types/jsonwebtoken,@types/bcrypt,@types/dotenvand@types/pgfor 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 --saveCreate a
srcdirectory for your source code.Inside the
srcdirectory, create aserver.tsfile 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:
- Entities: Domain models and business rules.
- Use cases: Application-specific business rules.
- Controllers: Handle requests and responses.
- Repositories: Database and external interfaces.
- Services: Manage external services like AWS S3, Twilio, etc.
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:
domain: Contains theUserentity and any business rules related to authentication.usecases: Contains the use cases for authentication, such asSignupandLogin.interface: Contains the Express.js server code and its associated controllers.
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:
Inside the
srcdirectory, create a new directory calledrepositories.Inside the
repositoriesdirectory, create a file calledUserRepository.ts.In the
UserRepository.tsfile, import the necessary dependencies:import { getRepository, Repository } from 'typeorm'; import { User } from '../domain/entities/User';Define a class called
UserRepositoryand export it:export class UserRepository { private repository: Repository<User>; constructor() { this.repository = getRepository(User); } }Add methods to the
UserRepositoryclass 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
getRepositoryfunction is provided by TypeORM, which is a popular Object-Relational Mapping (ORM) library for TypeScript and JavaScript.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:
Inside the
src/usecasesdirectory, create a file calledSignup.ts.In the
Signup.tsfile, import the necessary dependencies:import { UserRepository } from '../repositories/UserRepository'; import { User } from '../domain/entities/User'; import { hashPassword } from '../utils/passwordUtils';Define a class called
Signupand export it:export class Signup { private userRepository: UserRepository; constructor(userRepository: UserRepository) { this.userRepository = userRepository; } }Add a
executemethod to theSignupclass 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
hashPasswordfunction is a utility function that hashes the user’s password before storing it in the database. You can implement this function using a library likebcrypt.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:
Inside the
src/interfacedirectory, create a file calledAuthController.ts.In the
AuthController.tsfile, import the necessary dependencies:import { Request, Response } from 'express'; import { Signup } from '../usecases/Signup'; import { UserRepository } from '../repositories/UserRepository';Define a class called
AuthControllerand export it:export class AuthController { private signup: Signup; constructor() { this.signup = new Signup(new UserRepository()); } }Add methods to the
AuthControllerclass 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
signupHandlermethod calls theexecutemethod of theSignupuse case to create a new user. If the signup is successful, it responds with the created user; otherwise, it returns a 500 error.Define the routes for authentication in the
registerRoutesmethod of theAppclass in theserver.tsfile. For example, you can add the following route for the signup endpoint:router.post('/signup', authController.signupHandler.bind(authController));Here,
authController.signupHandleris bound to the route handler to maintain the correct context when handling the request.Save all the files and start the server using
npm startoryarn 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