Modern, secure, and production-ready authentication system built with NestJS, TypeORM, MariaDB, and JWT.
- Password Hashing: bcrypt with 12 rounds (industry standard)
- JWT Authentication: Secure token-based authentication
- Account Locking: Automatic lock after 5 failed login attempts (15 minutes)
- Password Requirements: Strong password validation
- Email Normalization: Automatic lowercase and trim
- SQL Injection Protection: TypeORM parameterized queries
- XSS Protection: Input validation and sanitization
- CORS Configuration: Configurable cross-origin resource sharing
- User Registration with validation
- User Login with secure password verification
- Protected routes with JWT Guards
- User profile endpoint
- Login attempt tracking
- Account lock/unlock mechanism
- Email uniqueness validation
- Framework: NestJS 11
- Language: TypeScript 5
- Database: MariaDB/MySQL
- ORM: TypeORM
- Authentication: Passport JWT
- Validation: class-validator & class-transformer
- Password Hashing: bcrypt
- Node.js (v18 or higher)
- npm or yarn
- MariaDB or MySQL (v10.5+)
git clone https://github.com/sw3do/example-user-backend-nestjs.git
cd example-user-backend-nestjs
npm installOption A: Using Docker (Recommended)
docker run -d \
--name mariadb \
-e MYSQL_ROOT_PASSWORD=123123 \
-e MYSQL_DATABASE=nestjs_auth \
-p 3306:3306 \
mariadb:latestOption B: Local Installation
# macOS
brew install mariadb
brew services start mariadb
# Ubuntu/Debian
sudo apt install mariadb-server
sudo systemctl start mariadb
# Create database
mysql -u root -p -e "CREATE DATABASE nestjs_auth CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"Copy the example env file and configure:
cp .env.example .envEdit .env:
NODE_ENV=development
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=your_password
DB_NAME=nestjs_auth
JWT_SECRET=change-this-to-a-very-secure-random-string-min-32-chars
JWT_EXPIRES_IN=7d
CORS_ORIGIN=http://localhost:3000
PORT=3000JWT_SECRET to a strong, random string in production!
# Development mode with hot reload
npm run start:dev
# Production mode
npm run build
npm run start:prodThe API will be available at http://localhost:3000
http://localhost:3000/api
POST /api/auth/register
Request:
{
"email": "user@example.com",
"password": "SecureP@ss123",
"firstName": "John",
"lastName": "Doe"
}Response (201):
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}
}POST /api/auth/login
Request:
{
"email": "user@example.com",
"password": "SecureP@ss123"
}Response (200):
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}
}GET /api/auth/profile
Headers:
Authorization: Bearer <your_access_token>
Response (200):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}Register:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "Test@123456",
"firstName": "John",
"lastName": "Doe"
}'Login:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "Test@123456"
}'Get Profile:
curl -X GET http://localhost:3000/api/auth/profile \
-H "Authorization: Bearer YOUR_TOKEN_HERE"Run the automated test script:
chmod +x test-api.sh
./test-api.sh- β Minimum 8 characters
- β Maximum 128 characters
- β At least one uppercase letter
- β At least one lowercase letter
- β At least one number OR special character
- β Bcrypt hashing with 12 salt rounds
- π Login attempts are tracked
- π Account locks after 5 failed attempts
- π Lock duration: 15 minutes
- π Automatic unlock after duration
- π Failed attempts reset on successful login
- π« Token expiration: 7 days (configurable)
- π« Secret key from environment variables
- π« Bearer token authentication
- π« Token validation on protected routes
- πΎ UUID primary keys
- πΎ Unique email constraint
- πΎ Password field excluded from queries by default
- πΎ UTF-8MB4 charset (emoji support)
- πΎ Automatic timestamps
src/
βββ auth/ # Authentication module
β βββ dto/ # Data Transfer Objects
β β βββ register.dto.ts # Registration validation
β β βββ login.dto.ts # Login validation
β βββ guards/ # Route guards
β β βββ jwt-auth.guard.ts # JWT authentication guard
β βββ strategies/ # Passport strategies
β β βββ jwt.strategy.ts # JWT strategy implementation
β βββ auth.controller.ts # Auth endpoints
β βββ auth.service.ts # Auth business logic
β βββ auth.module.ts # Auth module definition
βββ users/ # Users module
β βββ entities/ # Database entities
β β βββ user.entity.ts # User entity with hooks
β βββ users.service.ts # User business logic
β βββ users.module.ts # Users module definition
βββ app.module.ts # Root module
βββ main.ts # Application entry point
| Variable | Description | Default | Required |
|---|---|---|---|
NODE_ENV |
Environment mode | development |
No |
PORT |
Server port | 3000 |
No |
DB_HOST |
Database host | localhost |
No |
DB_PORT |
Database port | 3306 |
No |
DB_USERNAME |
Database username | root |
Yes |
DB_PASSWORD |
Database password | - | Yes |
DB_NAME |
Database name | nestjs_auth |
Yes |
JWT_SECRET |
JWT secret key | - | Yes |
JWT_EXPIRES_IN |
Token expiration | 7d |
No |
CORS_ORIGIN |
Allowed CORS origin | http://localhost:3000 |
No |
Create docker-compose.yml:
version: '3.8'
services:
mariadb:
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: 123123
MYSQL_DATABASE: nestjs_auth
ports:
- "3306:3306"
volumes:
- mariadb_data:/var/lib/mysql
volumes:
mariadb_data:Run:
docker-compose up -d400 Bad Request - Validation Error
{
"statusCode": 400,
"message": [
"Password must be at least 8 characters long",
"Email must be a valid email"
],
"error": "Bad Request"
}401 Unauthorized - Invalid Credentials
{
"statusCode": 401,
"message": "Invalid credentials",
"error": "Unauthorized"
}409 Conflict - Email Already Exists
{
"statusCode": 409,
"message": "Email already exists",
"error": "Conflict"
}- Email: Valid email format, unique
- Password: 8-128 chars, uppercase, lowercase, number/special char
- First Name: 2-100 chars
- Last Name: 2-100 chars
- Email: Valid email format
- Password: Required string
# Development
npm run start:dev # Start with hot reload
# Production
npm run build # Build application
npm run start:prod # Run production build
# Code Quality
npm run format # Format code with Prettier
npm run lint # Lint code with ESLint
npm run lint:fix # Fix linting issuesImport these endpoints to Postman:
- Register:
POST http://localhost:3000/api/auth/register - Login:
POST http://localhost:3000/api/auth/login - Profile:
GET http://localhost:3000/api/auth/profile
Create api.http:
### Register
POST http://localhost:3000/api/auth/register
Content-Type: application/json
{
"email": "test@example.com",
"password": "Test@123456",
"firstName": "John",
"lastName": "Doe"
}
### Login
POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
"email": "test@example.com",
"password": "Test@123456"
}
### Profile (Replace TOKEN)
GET http://localhost:3000/api/auth/profile
Authorization: Bearer YOUR_TOKEN_HERE- Check MariaDB is running:
# macOS
brew services list
# Linux
sudo systemctl status mariadb
# Docker
docker ps- Verify credentials in
.env - Check database exists:
mysql -u root -p -e "SHOW DATABASES;"Change port in .env:
PORT=3001- Check
JWT_SECRETis set in.env - Verify token hasn't expired (default 7 days)
- Check Authorization header format:
Bearer <token>
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License.
Created by @sw3do with β€οΈ using NestJS
β If you find this project helpful, please give it a star!