DEV Community

t sriya
t sriya

Posted on

Part 2 : Building an Authentication System from Scratch

User Registration & Secure Password Hashing with bcrypt

In the previous article, we built the backend foundation by setting up Express.js, PostgreSQL, environment variables, and a clean layered architecture.

With the backend ready, it's time to implement the first authentication feature—User Registration.

Although registration appears straightforward, it involves much more than simply storing user details in a database. A secure registration system must validate user input, prevent duplicate accounts, protect passwords, and ensure that sensitive information is never exposed.

In this article, we'll build the complete registration workflow while following security best practices.


Registration Flow

The registration process follows a layered architecture, where each layer has a single responsibility.

Client
   │
   ▼
Routes
   │
   ▼
Controller
   │
   ▼
Service
   │
   ▼
Repository
   │
   ▼
PostgreSQL
Enter fullscreen mode Exit fullscreen mode

The overall workflow is:

  1. The client submits the registration form.
  2. The controller receives the request.
  3. The service validates the data.
  4. The repository checks whether the email already exists.
  5. The password is securely hashed using bcrypt.
  6. The user is stored in PostgreSQL.
  7. A success response is returned to the client.

Why a Layered Architecture?

Instead of placing all the registration logic inside the controller, I divided the implementation into three layers.

Controller

Responsible only for:

  • Receiving the HTTP request
  • Calling the service layer
  • Returning the HTTP response

The controller should never contain business logic or database queries.


Service

The service contains the application's business logic.

For registration, it is responsible for:

  • Validating the request
  • Checking whether the email already exists
  • Hashing the password
  • Calling the repository to save the user

This layer acts as the brain of the application.


Repository

The repository communicates directly with PostgreSQL.

Its responsibilities include:

  • Checking if a user already exists
  • Creating a new user
  • Executing SQL queries

Keeping SQL isolated inside repositories makes the application easier to maintain and test.


Controller

The controller receives the registration request and forwards the data to the service layer.

The controller itself performs very little work.

Its responsibility is simply to:

  • Extract the request body
  • Call the service
  • Return either a success or an error response

This keeps controllers lightweight and easy to understand.


Service

The service contains the actual registration workflow.

The registration service performs the following steps:

  1. Check whether the email already exists.
  2. Generate a secure password hash.
  3. Create the user in PostgreSQL.
  4. Return the newly created user.

Because all business rules live inside the service layer, future changes become much easier.

For example, adding email verification later would require changes only inside the service, without affecting controllers or repositories.


Repository

The repository is responsible only for database communication.

Typical repository functions include:

  • findByEmail()
  • createUser()

Keeping SQL queries isolated improves readability and keeps the service layer database-agnostic.


Why Password Hashing is Necessary

One of the biggest mistakes an application can make is storing passwords in plain text.

Imagine a database leak.

If passwords are stored as plain text, every user's credentials become immediately visible.

Instead, passwords should always be transformed into a secure one-way hash before being stored.

This is exactly why we use bcrypt.


Why bcrypt?

bcrypt is one of the most trusted password hashing libraries available for Node.js.

Unlike encryption, hashing is a one-way operation.

This means:

  • The original password cannot be recovered.
  • Even the application itself cannot view the user's password.
  • Only password verification is possible.

How bcrypt Works

When a user registers, bcrypt performs several operations internally.

Password
    │
    ▼
Generate Random Salt
    │
    ▼
Password + Salt
    │
    ▼
Multiple Hashing Rounds
    │
    ▼
Store Hash in Database
Enter fullscreen mode Exit fullscreen mode

Each password receives its own randomly generated salt before hashing.

Because of this:

  • Two users with the same password will have completely different hashes.
  • Rainbow table attacks become ineffective.
  • Brute-force attacks become significantly slower due to bcrypt's configurable cost factor.

Password Verification During Login

During login, the user enters their password as plain text.

bcrypt then:

  1. Reads the stored hash.
  2. Extracts the embedded salt.
  3. Hashes the entered password using the same salt.
  4. Compares the generated hash with the stored hash.

If both hashes match, the user is successfully authenticated.

const isMatch = await bcrypt.compare(
    enteredPassword,
    storedHash
);
Enter fullscreen mode Exit fullscreen mode

One of bcrypt's biggest advantages is that developers never need to manually manage salts or compare hashes—the library handles the entire verification process securely.


Security Benefits of bcrypt

Using bcrypt provides several important security advantages.

✅ Passwords are never stored in plain text.

✅ Every password uses a unique random salt.

✅ Identical passwords generate different hashes.

✅ Brute-force attacks become significantly slower.

✅ Rainbow table attacks are mitigated.

These features make bcrypt one of the industry standards for password protection.


Testing the Registration Flow

Once the backend implementation was complete, I verified the registration API using Postman.

Request

POST /api/auth/register
Enter fullscreen mode Exit fullscreen mode
{
  "username": "Sriya",
  "email": "sriya@gmail.com",
  "password": "Password123"
}
Enter fullscreen mode Exit fullscreen mode

Response

{
  "success": true,
  "user": {
    "id": 1,
    "username": "Sriya",
    "email": "sriya@gmail.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the response never includes the password or its hash.

Only non-sensitive user information is returned to the client.


What's Next?

Now that users can securely register and their passwords are safely stored, the next step is allowing them to authenticate.

In the next article, we'll build the Login Flow, where we'll:

  • Verify user credentials
  • Compare passwords using bcrypt
  • Generate JWT Access Tokens
  • Generate Refresh Tokens
  • Understand how JWT authentication works internally

Continue Reading Part 3- https://dev.to/t_sriya_2af6abc7e8d4e87da/part-3-building-an-authentication-system-from-scratch-backend-setup-4119

Live App: https://auth-flow-five-iota.vercel.app/auth/
Backend API: https://auth-flow-backend-1v2h.onrender.com/
github url: https://github.com/sriyaT/Auth-Flow

Connect : LinkedIn : https://www.linkedin.com/in/t-sriya-b4234510a/, github : https://github.com/sriyaT

Author: Sriya T.

Top comments (0)