Skip to content

Sainava/Acquisitions

Repository files navigation

πŸ” Acquisitions API

A production-ready, secure Node.js/Express REST API for user authentication, role-based access control, and advanced bot protection. Built with modern DevOps practices: containerized workflows, automated CI/CD pipelines, comprehensive test coverage, and serverless database integration.

Perfect for: SaaS platforms, admin dashboards, microservices that need bulletproof auth + security.


✨ Core Features

πŸ”‘ Authentication & Authorization

  • JWT-based auth with secure HTTP-only cookies (15-min expiry, SameSite=Strict)
  • Password hashing via bcrypt (10 rounds)
  • Role-based access control (RBAC): Admin, User, Guest
  • Token verification & expiration handling on protected routes

πŸ›‘οΈ Advanced Security

  • Arcjet integration: Multi-layer threat detection
    • Bot detection (blocks non-whitelisted bots; allows Google/Bing crawlers)
    • DDoS shield (SQL injection, XSS, common attack patterns)
    • Sliding-window rate limiting (role-based: 5 req/min for guests, 10 for users, 20 for admins)
  • Helmet.js: Secure HTTP headers (CSP, HSTS, X-Frame-Options, etc.)
  • CORS: Strict origin validation
  • Input validation: Zod schemas on all endpoints (email, password strength, ID validation)

πŸ“Š Observability & Logging

  • Winston: Structured JSON logging to disk + console
    • Error logs: logs/error.log
    • Combined logs: logs/combined.log
  • Morgan: HTTP request/response logging stream β†’ Winston
  • Health check endpoint: Uptime, status, timestamp

πŸš€ Modern Stack

  • Express 5: Latest features with async/await middleware
  • Drizzle ORM: Type-safe, edge-friendly database queries
  • Neon PostgreSQL: Serverless, branch-based development, auto-scaling
  • Node 22: Native ESM, latest performance improvements

πŸ“¦ Containerized & Cloud-Ready

  • Multi-stage Dockerfile: Optimized prod & dev images (Alpine Linux)
  • Docker Compose: Dev with Neon Local, Prod with Neon Cloud
  • Docker Hub: Automated pushes for main branch (multi-platform: amd64, arm64)
  • Hot-reload dev environment: File watching with --watch flag

βœ… Testing & Quality

  • Jest with ESM support (NODE_OPTIONS='--experimental-vm-modules')
  • Supertest: HTTP assertion library for endpoint testing
  • Coverage reports: LCOV HTML reports; tracked across runs
  • ESLint + Prettier: Auto-formatting and linting on every PR

πŸ”„ Automated CI/CD Pipelines

See CI/CD Workflows below.


πŸ› οΈ Tech Stack

Core Dependencies

Package Version Purpose
express ^5.1.0 HTTP framework
@neondatabase/serverless ^1.0.2 Neon DB client
drizzle-orm ^0.44.6 Type-safe ORM
@arcjet/node ^1.0.0-beta.13 Bot/rate-limit protection
helmet ^8.1.0 HTTP header security
bcrypt ^6.0.0 Password hashing
jsonwebtoken ^9.0.2 JWT signing/verification
zod ^4.1.12 Input schema validation
winston ^3.18.3 Structured logging
morgan ^1.10.1 HTTP request logging
cookie-parser ^1.4.7 Cookie middleware
cors ^2.8.5 CORS middleware
dotenv ^17.2.3 Environment variables

Dev Dependencies

  • jest, supertest β€” Testing framework & HTTP assertions
  • drizzle-kit β€” DB migrations & schema generation
  • eslint, prettier β€” Code quality & formatting

πŸš€ Quick Start

Prerequisites

  • Node.js 20+ (or 22 recommended)
  • npm or yarn
  • Docker & Docker Compose (for containerized runs)
  • PostgreSQL or Neon account (for database)

1️⃣ Clone & Install

git clone https://github.com/Sainava/Acquisitions.git
cd Acquisitions
npm install

2️⃣ Environment Setup

Create .env (or .env.development/.env.production):

# Core
PORT=3000
NODE_ENV=development

# Database (Neon Cloud or local Postgres)
DATABASE_URL=postgres://username:password@host:5432/database?sslmode=require

# Security
JWT_SECRET=your-super-secret-key-change-in-production

# Arcjet Bot Protection
ARCJET_KEY=your-arcjet-site-key

# Optional: Neon Local Development
NEON_LOCAL=false
NEON_API_KEY=your-neon-api-key
NEON_PROJECT_ID=your-neon-project-id

3️⃣ Run Locally (Development Mode)

# Start dev server with auto-reload
npm run dev

# βœ… Server runs on http://localhost:3000

4️⃣ Run Tests

# Run Jest suite + coverage report
npm test

# βœ… Coverage HTML: open coverage/lcov-report/index.html

5️⃣ Code Quality

# Lint check
npm run lint

# Auto-fix ESLint & Prettier
npm run lint:fix
npm run format

🐳 Docker Setup

Development: Local Database with Neon Local

Benefits: Ephemeral branches, zero cloud cost, offline-friendly

npm run dev:docker

This runs scripts/dev.sh, which:

  1. Spins up Neon Local proxy (port 5432)
  2. Starts app container with hot-reload
  3. Runs schema migrations automatically
  4. Streams logs to foreground

Access:

Stop: Ctrl+C or docker compose -f docker-compose.dev.yml down

Production: Neon Cloud Database

Benefits: Scalable, secure, managed backups, CI/CD ready

npm run prod:docker

This runs scripts/prod.sh, which:

  1. Builds optimized production image (no dev deps)
  2. Connects to Neon Cloud via DATABASE_URL
  3. Runs migrations inside container
  4. Exposes single container on port 3000

Customize: Edit docker-compose.prod.yml or pass env vars:

docker compose -f docker-compose.prod.yml up \
  -e DATABASE_URL="postgres://..." \
  -e JWT_SECRET="..." \
  -e ARCJET_KEY="..."

Deploy to: AWS ECS, Google Cloud Run, Azure Container Instances, Render, Heroku, Railway, etc.


πŸ“‘ API Endpoints

Health & Status

GET /health

Returns: { status: "OK", timestamp: "...", uptime: 42.3 }

GET /api

Returns: { message: "Acquisitions API is running" }

Authentication

Sign Up

POST /api/auth/sign-up
Content-Type: application/json

{
  "name": "John Doe",
  "email": "john@example.com",
  "password": "SecurePass123",
  "role": "user"
}

Response (201):

{
  "message": "User registered successfully",
  "user": {
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com",
    "role": "user"
  }
}

Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict; Max-Age=900

Sign In

POST /api/auth/sign-in
Content-Type: application/json

{
  "email": "john@example.com",
  "password": "SecurePass123"
}

Response (200): Same as sign-up + cookie set

Sign Out

POST /api/auth/sign-out
Cookie: token=<jwt>

Response (200): { message: "Sign-out successful" } + cookie cleared

User Management (Protected Routes)

All require: Valid JWT cookie + "Authorization: Bearer <token>" header (or cookie)

Get All Users

GET /api/users
Cookie: token=<jwt>

Auth: Admin only

Response (200):

{
  "message": "Successfully retrieved users",
  "users": [...],
  "count": 5
}

Get User by ID

GET /api/users/1
Cookie: token=<jwt>

Auth: Any authenticated user

Response (200): User object with id, name, email, role, createdAt, updatedAt

Update User

PUT /api/users/1
Cookie: token=<jwt>
Content-Type: application/json

{
  "name": "Jane Doe",
  "email": "jane@example.com"
}

Auth: Users can update own profile; admins can update any

Response (200): Updated user object

Delete User

DELETE /api/users/2
Cookie: token=<jwt>

Auth: Admin only (cannot delete own account)

Response (200): Deleted user object


πŸ” Security Details

Password Storage

  • Hashed with bcrypt (10 rounds, ~100ms per hash)
  • Never stored in plaintext
  • Compared securely on sign-in

JWT Tokens

  • Payload: { id, email, role }
  • Expiry: 1 day
  • Secret: Loaded from JWT_SECRET env var
  • Storage: Secure HTTP-only cookie (can't be accessed by JavaScript)

Rate Limiting (Arcjet)

Role Limit Window
Guest 5 req 1 min
User 10 req 1 min
Admin 20 req 1 min

Bot Detection (Arcjet)

  • βœ… Whitelisted: Google, Bing, other search engines
  • ❌ Blocked: Unknown bots
  • πŸ›‘οΈ Shield: Protects against SQL injection, XSS, CSRF

CORS

  • Strict origin validation
  • Credentials allowed (cookies)
  • Safe methods: GET, HEAD, OPTIONS

πŸ§ͺ Testing

Run Tests

npm test

Test File Location

Coverage

npm test
# βœ… Generates: coverage/lcov-report/index.html
# Open in browser to view line/branch/function coverage

Test Environment

  • NODE_ENV=test (Arcjet bypassed for deterministic tests)
  • DATABASE_URL=postgres://testuser:testpass@localhost/testdb (in-memory or docker-based)

Add More Tests

describe('POST /api/auth/sign-up', () => {
  it('should register a new user', async () => {
    const response = await request(app)
      .post('/api/auth/sign-up')
      .send({
        name: 'Test User',
        email: 'test@example.com',
        password: 'TestPass123',
      })
      .expect(201);

    expect(response.body.user.email).toBe('test@example.com');
    expect(response.headers['set-cookie']).toBeDefined(); // JWT cookie
  });
});

πŸ—„οΈ Database & Migrations

Schema

Single table: users

Column Type Default
id serial (PK) auto-increment
name varchar(255) β€”
email varchar(255) β€”
password varchar(255) β€”
roll varchar(50) 'user'
created_at timestamp now()
updated_at timestamp now()

Drizzle Migrations

Generate migration from TypeScript schema:

npm run db:generate

Run pending migrations:

npm run db:migrate

Open Drizzle Studio (GUI for DB):

npm run db:studio

Drizzle Configuration

drizzle.config.js points to:

  • Schema: src/models/user.model.js
  • Output: drizzle/ (migrations + metadata)
  • Database: Neon Cloud or Local via DATABASE_URL

πŸ”„ CI/CD Workflows

All pipelines automate on push to main or staging + pull requests.

1️⃣ Lint & Format (.github/workflows/lint-and-format.yml)

Triggers: Every PR + push to main/staging

Steps:

  1. Checkout code
  2. Setup Node.js 20
  3. Install dependencies
  4. Run ESLint (npm run lint)
  5. Run Prettier check (npm run format:check)
  6. Fail if: Lint errors or formatting issues

Annotation: "Run npm run lint:fix && npm run format to fix"

2️⃣ Tests (.github/workflows/tests.yml)

Triggers: Every PR + push to main/staging

Steps:

  1. Checkout code
  2. Setup Node.js 20
  3. Install dependencies
  4. Set NODE_ENV=test + NODE_OPTIONS='--experimental-vm-modules'
  5. Run Jest suite (npm test)
  6. Upload coverage reports (30-day retention)
  7. Generate test summary in PR
  8. Fail if: Any test fails

Artifacts: Coverage HTML available for 30 days

3️⃣ Docker Build & Push (.github/workflows/docker-build-and-push.yml)

Triggers: Push to main only (or manual via workflow_dispatch)

Prerequisites:

  • Docker Hub credentials in GitHub Secrets:
    • DOCKER_USERNAME
    • DOCKER_PASSWORD

Steps:

  1. Checkout code
  2. Setup Docker Buildx (multi-platform builder)
  3. Authenticate to Docker Hub
  4. Extract metadata (branch, SHA, latest tag, timestamp)
  5. Build & push image
    • Platforms: linux/amd64 (Intel/AMD) + linux/arm64 (Apple Silicon)
    • Cache: GitHub Actions cache for faster rebuilds
  6. Publish summary to GitHub

Image Tags:

  • tag:main (branch name)
  • tag:sha-abc123def (commit SHA, truncated)
  • tag:latest (stable alias)
  • tag:prod-20260318-140530 (timestamp for audit)

Push to: docker.io/<DOCKER_USERNAME>/acquisitions-app

Uses: docker/build-push-action@v5


πŸ“ Project Structure

.
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.js                 # Entry point (loads .env)
β”‚   β”œβ”€β”€ server.js               # HTTP server bind
β”‚   β”œβ”€β”€ app.js                  # Express app + middleware setup
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ logger.js           # Winston logger config
β”‚   β”‚   β”œβ”€β”€ database.js         # Drizzle + Neon connection
β”‚   β”‚   β”œβ”€β”€ arcjet.js           # Arcjet rules (bot, shield, rate-limit)
β”‚   β”‚   └── neon.local.js       # Neon Local dev mode config
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ auth.routes.js      # POST /api/auth/sign-{up,in,out}
β”‚   β”‚   └── users.routes.js     # GET/PUT/DELETE /api/users/:id
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   β”œβ”€β”€ auth.controller.js  # Sign up/in/out logic
β”‚   β”‚   └── users.controller.js # User CRUD handlers
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”œβ”€β”€ auth.service.js     # Auth business logic (hash, create, verify)
β”‚   β”‚   └── users.service.js    # User DB queries
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ auth.middleware.js  # JWT verify + role checks
β”‚   β”‚   └── security.middleware.js # Arcjet bot/rate/shield
β”‚   β”œβ”€β”€ models/
β”‚   β”‚   └── user.model.js       # Drizzle schema
β”‚   β”œβ”€β”€ validations/
β”‚   β”‚   β”œβ”€β”€ auth.validation.js  # Zod schemas for sign-up/in
β”‚   β”‚   └── users.validation.js # Zod schemas for user operations
β”‚   └── utils/
β”‚       β”œβ”€β”€ jwt.js              # JWT sign/verify helpers
β”‚       β”œβ”€β”€ cookies.js          # Cookie set/clear helpers
β”‚       └── format.js           # Validation error formatter
β”œβ”€β”€ tests/
β”‚   └── app.test.js             # Jest endpoint tests
β”œβ”€β”€ drizzle/
β”‚   β”œβ”€β”€ 0000_aberrant_outlaw_kid.sql # Migration file
β”‚   └── meta/
β”‚       β”œβ”€β”€ _journal.json       # Migration history
β”‚       └── 0000_snapshot.json  # Schema snapshot
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ dev.sh                  # Docker dev bootstrap
β”‚   └── prod.sh                 # Docker prod bootstrap
β”œβ”€β”€ .github/workflows/
β”‚   β”œβ”€β”€ lint-and-format.yml     # ESLint + Prettier CI
β”‚   β”œβ”€β”€ tests.yml               # Jest CI
β”‚   └── docker-build-and-push.yml # Multi-platform Docker push
β”œβ”€β”€ Dockerfile                  # Multi-stage build (dev + prod)
β”œβ”€β”€ docker-compose.dev.yml      # Dev: Neon Local + app
β”œβ”€β”€ docker-compose.prod.yml     # Prod: app only
β”œβ”€β”€ eslint.config.js            # ESLint rules
β”œβ”€β”€ jest.config.mjs             # Jest config (v30, ESM)
β”œβ”€β”€ drizzle.config.js           # Drizzle migration config
β”œβ”€β”€ package.json                # Scripts + dependencies
β”œβ”€β”€ README.md                   # This file
└── Docker_documentation.md     # Detailed Docker runbook

πŸ‘¨β€πŸ’» Development

Local Setup & Hot Reload

npm run dev
# βœ… Auto-restarts on file changes

Linting & Formatting

# Check code style
npm run lint
npm run format:check

# Auto-fix issues
npm run lint:fix
npm run format

Database Migrations

# Generate migration from schema changes
npm run db:generate

# Apply migrations
npm run db:migrate

# Open interactive DB studio
npm run db:studio

Environment Variables

Create .env in root:

PORT=3000
NODE_ENV=development
DATABASE_URL=postgres://...
JWT_SECRET=your-secret
ARCJET_KEY=your-key

Never commit .env files; add to .gitignore:

.env
.env.local
.env.*.local

πŸš€ Deployment

Option 1: Docker Hub + Cloud Platform

  1. Build & Push (automated on main via CI/CD):

    # Manual push (if needed):
    docker buildx build --push \
      --platform linux/amd64,linux/arm64 \
      -t docker.io/yourname/acquisitions-app:latest .
  2. Pull & Deploy to AWS ECS, Render, Railway, etc.:

    docker pull docker.io/yourname/acquisitions-app:latest
    docker run \
      -e DATABASE_URL="postgres://..." \
      -e JWT_SECRET="..." \
      -e ARCJET_KEY="..." \
      -p 3000:3000 \
      docker.io/yourname/acquisitions-app:latest

Option 2: Node.js + Systemd on VPS

# On remote server
git clone <repo> /opt/acquisitions
cd /opt/acquisitions
npm ci --omit=dev
npm run db:migrate

# Create systemd service
sudo nano /etc/systemd/system/acquisitions.service
[Unit]
Description=Acquisitions API
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/acquisitions
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
User=appuser
Environment="NODE_ENV=production"

[Install]
WantedBy=multi-user.target
sudo systemctl enable acquisitions
sudo systemctl start acquisitions
sudo systemctl status acquisitions

Option 3: Heroku / Render / Railway

Connect GitHub repo; platform auto-deploys on push to main.

Set env vars in platform dashboard:

  • DATABASE_URL β†’ Neon Cloud connection string
  • JWT_SECRET β†’ Secure random string
  • ARCJET_KEY β†’ Your Arcjet key
  • NODE_ENV=production

πŸ› Troubleshooting

"Cannot find module '#models/user.model.js'"

  • Ensure package.json has correct "imports" mapping
  • Clear cache: rm -rf node_modules && npm ci

Arcjet blocks my Postman requests

  • In dev, Arcjet may flag client tools as bots
  • Set ARCJET_MODE=DRY_RUN temporarily (logs, doesn't block)
  • Or: Whitelist your IP in Arcjet dashboard

Tests fail in CI but pass locally

  • Ensure NODE_ENV=test and DATABASE_URL set in workflow
  • Coverage artifacts may reveal missing mocks or edge cases

Docker build fails: "Database URL required"

  • Ensure .env.development or .env.production exists OR
  • Pass via docker run -e DATABASE_URL="..."

Can't connect to Neon Local

  • Ensure NEON_LOCAL=true in .env.development
  • Check running containers: docker ps
  • View container logs: docker logs neon-local

πŸ“– Resources


πŸ“ License

ISC License β€” See repository for details.


Need help? Open a GitHub issue or check the Docker_documentation.md for advanced setup.

Happy building! πŸš€

About

Acquisitions API is a secure, modern Node.js backend built with Express 5, Drizzle ORM, Neon, and Arcjet. It provides robust authentication, rate limiting, and bot protection, with Dockerized environments, CI/CD via GitHub Actions, and full test coverage for reliability.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors