Setting up a scalable authentication backend using Node.js, Express.js, PostgreSQL, and a layered architecture.
Introduction__
Authentication is one of the most essential features of any modern web application. While many projects rely on third-party providers such as Firebase Auth, Auth0, or Clerk, I wanted to understand what actually happens behind the scenes when a user registers, logs in, resets their password, or logs out.
To achieve that, I decided to build a complete authentication system from scratch.
In this project, I implemented the entire authentication flow using:
React
Node.js
Express.js
PostgreSQL
JWT
Redux Toolkit
Tailwind CSS
Resend
The goal wasn't just to make authentication work—it was to understand the architecture, security considerations, and communication between the frontend, backend, and database.
This article is the first part of the series, where we'll build the backend foundation that powers the entire authentication system.
Project Structure
I prefer keeping both the frontend and backend inside a single project repository. This makes development, version control, and deployment much easier to manage.
Auth-Flow
│
├── auth-backend
│
└── auth-frontend
Inside the backend, I initialized a new Node.js application.
mkdir auth-project
cd auth-project
mkdir auth-backend
mkdir auth-frontend
cd auth-backend
mkdir server
cd server
npm init -y
Installing Dependencies
Next, I installed the packages required for building the authentication APIs.
npm install express pg bcrypt dotenv cors
npm install -D nodemon
Each package serves a specific purpose.
Package Purpose
Express Build REST APIs
pg Connect Node.js with PostgreSQL
bcrypt Securely hash passwords
dotenv Manage environment variables
cors Enable frontend-backend communication
nodemon Automatically restart the server during development
Instead of installing unnecessary libraries upfront, I only installed the packages required for the current stage of development. Additional libraries such as JWT and Resend will be introduced later as the project evolves.
Organizing the Backend
As applications grow, keeping everything inside a single file quickly becomes difficult to maintain.
To keep the project modular and scalable, I organized the backend into multiple layers.
server
│
├── src
│
│── controllers
│── services
│── repositories
│── middleware
│── routes
│── db
│── utils
│
├── server.js
└── package.json
Each folder has a clearly defined responsibility.
Controllers handle incoming HTTP requests and responses.
Services contain business logic.
Repositories interact with PostgreSQL.
Routes define API endpoints.
Middleware contains reusable request-processing logic.
DB manages database connections.
Utils stores helper functions.
This layered architecture keeps responsibilities separated and makes the codebase much easier to maintain and extend.
Setting Up PostgreSQL
Since user information needs to persist across sessions, I chose PostgreSQL as the database.
First, create the database.
CREATE DATABASE auth_project;
Then create the users table.
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(200) NOT NULL,
email VARCHAR(250) NOT NULL UNIQUE,
password_hash VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Notice that the table stores password_hash instead of the user's original password. This is one of the most important security practices when building authentication systems.
Environment Variables
Sensitive information such as database credentials should never be hardcoded inside the application.
Instead, I created a .env file.
PORT=5000
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password
DB_NAME=auth_project
The .env file is excluded from version control.
node_modules
.env
This prevents sensitive credentials from being pushed to GitHub.
Creating the Database Connection
Instead of creating a new PostgreSQL connection inside every repository, I created a reusable database connection using pg.Pool.
const { Pool } = require("pg");
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
module.exports = pool;
This single connection pool is shared throughout the application, improving performance and reducing resource usage.
Creating the Express Application
Next, I configured the Express server.
const express = require("express");
const cors = require("cors");
const app = express();
app.use(express.json());
app.use(
cors({
origin: "http://localhost:5173",
credentials: true,
})
);
module.exports = app;
At this stage, the server is capable of:
Parsing JSON request bodies
Accepting requests from the React frontend
Preparing for future authentication middleware
Server Entry Point
The application starts from server.js.
require("dotenv").config();
const app = require("./src/app");
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Running
npm run dev
starts the backend server.
Server running on port 5000
Verifying the Backend
Before implementing authentication, I always verify that the server is functioning correctly.
I added a simple health-check endpoint.
router.get("/", (req, res) => {
res.json({
status: "ok",
});
});
Request:
GET /health
Response:
{
"status": "ok"
}
This confirms that the backend is configured correctly before moving on to authentication.
Why I Chose a Layered Architecture
One mistake I often see in beginner projects is placing SQL queries directly inside controllers.
Instead, I separated responsibilities into three layers.
Controller
│
▼
Service
│
▼
Repository
│
▼
PostgreSQL
Controller
Receives the HTTP request and returns the HTTP response.
Service
Contains all business logic, including:
Email validation
Password hashing
JWT generation
Authentication rules
Repository
Handles database operations only.
This separation makes the project easier to:
Debug
Test
Scale
Maintain
As more authentication features are added, each layer continues to have a single responsibility.
Conclusion
With the backend infrastructure now in place, the application is ready to implement authentication features.
In the next article, we'll build the User Registration Flow, where we'll:
Validate user input
Prevent duplicate accounts
Securely hash passwords using bcrypt
Store users in PostgreSQL
Return a safe API response
Continue Reading Part 2: https://dev.to/t_sriya_2af6abc7e8d4e87da/part-2building-an-authentication-system-from-scratch-backend-setup-59gg
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)