Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

NestJSTypeScriptMongoDBAWS EC2DockerJWTSendGrid

⚡ Minify API

A production-grade REST API for URL shortening, click analytics, and bio page management — deployed on AWS EC2 with Docker.
Built with NestJS, TypeScript, and MongoDB.

Live DemoFrontend Repo


Highlights

🔗 URL Shortener — Public & authenticated short link creation with custom vanity codes, expiration support, and 302 redirect handling.

📊 Analytics Engine — Per-click event tracking with GeoIP resolution (country/city), browser breakdown, referrer capture, and dashboard-level aggregation.

🌳 Bio Pages — Linktree-style public profiles with custom usernames, editable bios, and link management.

🔐 Auth System — JWT + Passport with bcrypt hashing, CSPRNG reset tokens, SHA-256 token storage, and timing-attack mitigation.

👤 Role-Based AccessUSER and ADMIN roles with guard-based enforcement via custom @Roles() decorator.

☁️ Cloud-Native — Dockerized on AWS EC2, multi-stage builds, environment-driven config, CORS-whitelisted Vercel frontend.


Engineering Decisions

This section explains why things are built the way they are — not just what they do.

DecisionRationale
Global exception filter + response interceptorEvery response — success or error — follows the same { status, message, data } envelope. Frontend never has to guess the shape.
Optional JWT guard on URL shorteningAnonymous users can shorten links (lower friction), but authenticated users get ownership and custom codes. One endpoint, two experiences.
SHA-256 hashed reset tokens in DBEven if the database is compromised, reset tokens can't be reversed. Combined with 10-minute expiry and CSPRNG generation.
Async email dispatch (fire-and-forget)forgotPassword returns instantly regardless of whether the email is found. Prevents timing attacks that leak user existence.
Reserved short code setWords like admin, auth, dashboard, api are blocked from being used as short codes — prevents route collisions without complex routing.
GeoIP on redirect, not on creationClick analytics capture the visitor's location, not the link creator's. This gives meaningful geographic engagement data.
Multi-stage Docker buildBuilder stage compiles TS + prunes devDeps. Runner stage gets only dist/, node_modules/, and package.json — minimal attack surface, ~80% smaller image.
trust proxy enabledBehind AWS load balancers and reverse proxies, req.ip would return the proxy's IP. This ensures GeoIP and rate limiting work on real visitor IPs.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Vercel (Frontend SPA) │
│ github.com/Mahmoud142/minify-web │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS
┌────────────────────────────▼────────────────────────────────────┐
│ AWS EC2 Instance │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Docker Container (Node 20 Alpine) │ │
│ │ │ │
│ │ Morgan ─▶ Throttler ─▶ ValidationPipe ─▶ Auth Guards │ │
│ │ │ │ │
│ │ ┌──────────────────▼───────────────────┐ │ │
│ │ │ NestJS Modules │ │ │
│ │ │ │ │ │
│ │ │ Auth · URL · User · Linktree · Mail │ │ │
│ │ │ │ │ │
│ │ └──────────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ResponseInterceptor ◀───┘───▶ AllExceptionsFilter │ │
│ └────────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────▼────────────┐
│ MongoDB Atlas │
│ Users · URLs · Clicks │
│ Linktrees │
└─────────────────────────┘

Request Lifecycle

Request ─▶ Morgan Logger ─▶ ThrottlerGuard ─▶ ValidationPipe ─▶ Auth Guard
│
Response ◀── ResponseInterceptor ◀── Service ◀── Controller ◀───────┘
│
(on error)
└──▶ AllExceptionsFilter ──▶ Standardized error response

Tech Stack

LayerTechnology
RuntimeNode.js 20 (Alpine)
FrameworkNestJS 11
LanguageTypeScript 5 (ES2023, strict null checks)
DatabaseMongoDB + Mongoose 9
AuthPassport JWT · bcrypt · crypto (CSPRNG)
Validationclass-validator · class-transformer
EmailSendGrid
Analyticsgeoip-lite (IP → country/city)
Rate Limiting@nestjs/throttler (60 req/min global, 20/min redirect)
LoggingMorgan
TestingJest · Supertest (unit + e2e)
CloudAWS EC2 (API) · Vercel (Frontend)
ContainerizationDocker (multi-stage) · Docker Compose
Code QualityESLint 9 · Prettier

API Reference

Auth — POST /auth/*

EndpointDescription
/auth/signupRegister a new account
/auth/loginAuthenticate and receive JWT
/auth/forgot-passwordRequest 6-digit reset code via email
/auth/verify-reset-codeValidate the reset code
/auth/reset-passwordSet new password with verified code

URLs — /url/*

MethodEndpointAuthDescription
POST/url/shortenOptionalShorten a URL (auth = custom codes)
GET/url/my-urlsList your shortened URLs
GET/url/analyticsAggregate analytics across all your URLs
GET/url/:id/statsDetailed stats for a specific URL
DELETE/url/:idDelete URL + its click events
GET/url/:shortCodeRedirect to original URL (public)

Users — /user/*

MethodEndpointAuthDescription
GET/user/profileGet your profile
PATCH/user/:idUpdate own profile (or admin: any)
GET/user✅ AdminList all users
GET/user/:id✅ AdminGet user by ID
DELETE/user/:id✅ AdminDelete a user

Bio Pages — /minf/*

MethodEndpointAuthDescription
GET/minfGet your bio page (auto-created)
GET/minf/:usernameView public bio page
PATCH/minf/usernameUpdate username, bio, link titles
POST/minf/linksAdd a link
DELETE/minf/links/:linkIdRemove a link

Response Contract

Every response follows the same envelope — frontend never guesses:

// Success
{
"status": "success",
"message": "URL shortened successfully",
"data": { ... }
}
// Error
{
"status": "error",
"statusCode": 404,
"message": "URL not found or inactive"
}

Security

LayerImplementation
Password storagebcrypt, 10 salt rounds
Reset tokenscrypto.randomInt() (CSPRNG) → SHA-256 hashed → stored in DB
Token expiry10-minute TTL on reset codes
Timing attack defenseAsync email dispatch — response time doesn't reveal user existence
Enumeration defenseGeneric errors: "Code is invalid or has expired"
Input sanitizationwhitelist: true + forbidNonWhitelisted: true on all DTOs
CORSEnvironment-driven origin whitelist
Proxy awarenesstrust proxy for real IP behind AWS load balancers
Route collision guardReserved set blocks admin, auth, dashboard, etc. as short codes
Rate limitingGlobal 60/min + redirect-specific 20/min via @nestjs/throttler

Project Structure

src/
├── main.ts # Bootstrap — CORS, pipes, guards, filters
├── app.module.ts # Root module — wires all feature modules
├── app.controller.ts # Health check
│
├── auth/ # 🔐 Authentication & Authorization
│ ├── auth.controller.ts # signup, login, forgot/verify/reset password
│ ├── auth.service.ts # JWT signing, bcrypt, CSPRNG tokens
│ ├── strategies/ # Passport JWT strategy
│ ├── guards/ # JwtAuthGuard, OptionalJwtAuthGuard, RolesGuard
│ ├── decorators/ # @GetUser(), @Roles()
│ ├── enums/ # Role.USER, Role.ADMIN
│ ├── dto/ # SignupDto, LoginDto, ForgotPasswordDto, etc.
│ └── interfaces/ # JwtPayload, AuthResponse types
│
├── url/ # 🔗 URL Shortening & Analytics
│ ├── url.controller.ts # shorten, my-urls, analytics, stats, redirect
│ ├── url.service.ts # Short code gen, GeoIP tracking, aggregation
│ ├── dto/ # CreateUrlDto
│ └── schemas/ # Url schema, ClickEvent schema
│
├── user/ # 👤 User Management
│ ├── user.controller.ts # profile, update, admin CRUD
│ ├── user.service.ts # find, create, update, delete
│ ├── dto/ # UpdateUserDto
│ ├── schemas/ # User schema
│ └── interfaces/ # UserResponse types
│
├── linktree/ # 🌳 Bio Pages
│ ├── linktree.controller.ts # bio page + link CRUD
│ ├── linktree.service.ts # username management, public view
│ ├── dto/ # AddLinkDto, UpdateUsernameDto
│ └── schemas/ # Linktree schema
│
├── mail/ # 📧 Transactional Email
│ ├── mail.service.ts # SendGrid: reset + confirmation templates
│ └── mail.module.ts
│
└── common/ # 🛠️ Shared Infrastructure
├── interceptors/
│ └── response.interceptor.ts # Wraps all success responses
└── filters/
└── all-exceptions.filter.ts # Catches & standardizes all errors

Getting Started

Prerequisites

  • Node.js ≥ 20
  • MongoDB (local or Atlas)
  • SendGrid API key

Quick Start

# Clone
git clone https://github.com/Mahmoud142/minify-api.git
cd minify-api
# Install
npm install
# Configure
cp .env.example .env # then fill in your values# Run
npm run start:dev # http://localhost:3000

Environment Variables

VariableDescriptionRequired
DB_URIMongoDB connection string
JWT_SECRETSecret for signing JWT tokens
SENDGRID_KEYSendGrid API key
FRONTEND_URLFrontend origin (CORS + email links)
CORS_ORIGINComma-separated allowed originsOptional
PORTServer port (default: 3000)Optional

Scripts

CommandDescription
npm run start:devDev server with hot reload
npm run start:debugDev server with debugger
npm run buildCompile TypeScript → dist/
npm run start:prodRun production build
npm run testUnit tests
npm run test:e2eEnd-to-end tests
npm run test:covTests with coverage
npm run lintESLint with auto-fix

Deployment

Production — AWS EC2

Vercel (Frontend) ──HTTPS──▶ AWS EC2 (Docker → NestJS API) ──▶ MongoDB Atlas
  • Dockerized NestJS on EC2 with docker compose up --build -d
  • trust proxy enabled for real IP resolution behind load balancers
  • Environment-driven CORS whitelist for the Vercel frontend
  • Frontend source: Mahmoud142/minify-web

Docker

Multi-stage build for minimal production images:

# With Docker Compose (recommended)
docker compose up --build -d
# Or standalone
docker build -t minify-api .
docker run -p 3000:3000 --env-file .env minify-api

Stage 1 (Builder): Install all deps → compile TypeScript → prune devDependencies
Stage 2 (Runner): Copy only dist/, node_modules/, package.json into clean Alpine image


Related Repositories

RepositoryDescriptionStack
minify-apiBackend REST API (this repo)NestJS · TypeScript · MongoDB · AWS EC2
minify-webFrontend dashboardDeployed on Vercel

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/your-feature
  3. Commit with conventional messages: feat(url): add link expiration
  4. Push and open a Pull Request

Built with ❤️ by Mahmoud Abdellah

About

A fast and scalable URL shortening API built with Node.js, NestJS, and MongoDB

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages