Skip to content

CodeWithZezo/claude-api-challange

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 

Repository files navigation

🚀 Task Management System API

A production-grade REST API built from scratch as part of the Claude API Challenge — demonstrating real-world backend engineering with TypeScript, Express 5, MongoDB, and a clean modular architecture. Every line of business logic, every schema, every middleware, and every utility is hand-written — no boilerplate generators, no scaffolding tools.


📌 Table of Contents


🧭 Overview

This project implements the backend for a multi-tenant task management platform — think Jira or Linear, built ground-up. The system supports organizations with teams, projects, tasks with dependencies, role-based access control, webhooks, API key management, and a full audit trail.

What makes this production-grade:

  • Stateless JWT authentication with rotating refresh tokens stored in httpOnly cookies
  • Layered service architecture (Controller → Service → Model) with zero business logic leaking into controllers
  • Custom ServiceResult<T> pattern for consistent, typed error propagation without throwing across layers
  • A DatabaseManager class built on the raw MongoDB driver with connection pooling, exponential-backoff retry, slow-query detection, and periodic health checks — all sitting alongside Mongoose for the application layer
  • Soft-delete patterns across all entities (deletedAt field) for safe data recovery
  • Schema-level validation with Mongoose custom validators, not just app-level checks
  • Compound database indexes chosen to match the real query patterns of the application

🏗 Architecture

┌─────────────────────────────────────────────────────────┐
│                        Express App                       │
├──────────┬──────────────────────────────────────────────┤
│  Routes  │  /api/auth  │  /api/org  │  /api/...         │
├──────────┴──────────────────────────────────────────────┤
│              Middleware Layer                             │
│   authenticate()  │  authorize(...roles)  │  checkOrg()  │
├─────────────────────────────────────────────────────────┤
│              Module Layer (per domain)                   │
│   Controller  →  Service  →  Model (Mongoose Schema)     │
├─────────────────────────────────────────────────────────┤
│              Infrastructure Layer                        │
│   DatabaseManager (raw driver + pool + health checks)    │
│   JWTUtils  │  PasswordUtils  │  ServiceResult<T>        │
└─────────────────────────────────────────────────────────┘

Each domain (auth, org, user, task, project, team…) lives in its own module folder with its own controller, service, and route file. Nothing leaks between modules except through the shared model layer.


🛠 Tech Stack

Layer Technology Why
Runtime Node.js + TypeScript 5 Type-safe development, catches bugs at compile time
Framework Express 5 Latest stable, async error propagation built-in
Database MongoDB + Mongoose 9 Flexible document model fits multi-tenant hierarchy
Low-level DB MongoDB Driver (raw) Used in DatabaseManager for pool control & monitoring
Auth JWT (jsonwebtoken) Stateless, scalable; access + refresh token pair
Password bcrypt Adaptive hashing cost factor, industry standard
Dev Tools ts-node + nodemon Hot-reload TypeScript in development

📐 Data Models

The schema design reflects a real multi-tenant SaaS architecture:

Organization
  ├── plan: FREE | BASIC | PREMIUM
  ├── subdomain (unique, indexed, slug-validated)
  └── users[] → User
        ├── role: ADMIN | MANAGER | MEMBER
        ├── organizationId (ref)
        └── refreshTokens[]

Teams (within an org)
  └── TeamMembers (role: LEAD | MEMBER)

Projects (within an org)
  ├── status: ACTIVE | ARCHIVED | ON_HOLD
  └── Tasks
        ├── status: TODO | IN_PROGRESS | IN_REVIEW | DONE | CANCELLED
        ├── priority: LOW | MEDIUM | HIGH | URGENT
        ├── parentId (self-ref for subtasks)
        ├── TaskAssignments
        └── TaskDependencies

ApiKeys (org-scoped, key never exposed after creation)
Webhooks (org-scoped, secret never exposed after creation)
WebhookDeliveries (delivery status: PENDING | SUCCESS | FAILED | RETRYING)
AuditLogs (immutable append-only, tracks all mutations with IP + user agent)
Attachments
Comments

Every schema uses deletedAt: Date | null for soft deletes rather than hard removal — entities are logically deleted and can be recovered.


🔌 API Reference

Auth — /api/auth

Method Endpoint Auth Description
POST /register Public Register a new user (becomes ADMIN of their org)
POST /login Public Login, returns accessToken in body + refreshToken in httpOnly cookie
POST /refresh-token Cookie Rotate the refresh token, get a new access token
POST /logout Bearer Invalidates the refresh token server-side
POST /change-password Bearer Validates current password, rotates all refresh tokens on change
GET /profile Bearer Returns authenticated user profile (password excluded)

Token Strategy:

  • Access tokens are short-lived JWTs sent in the Authorization: Bearer header
  • Refresh tokens are long-lived, stored in the database, and delivered only via httpOnly; SameSite=Strict cookie — never accessible to JavaScript
  • On change-password, all refresh tokens for the user are revoked (force logout everywhere)

Organizations — /api/org

Method Endpoint Auth Description
POST / Bearer Create an organization (links to authenticated user)
GET /:id Bearer Fetch organization by ID
PATCH /:id Bearer Update org details (name, subdomain — duplicate check enforced)
DELETE /:id Bearer Soft-delete the organization, unlinks user

User — /api/user

Method Endpoint Auth Description
GET /me Bearer Get current user details

🔐 Security Design

Security was treated as a first-class concern, not an afterthought:

Authentication Middleware (auth.middleware.ts)

  • Extracts and verifies the Bearer token on every protected route
  • Re-fetches the user from the database on every request — if the account is deactivated, access is immediately denied even with a valid token
  • Three composable middleware functions: authenticate, authorize(...roles), and checkOrganization — applied selectively per route

Password Handling (password.utils.ts)

  • Passwords are hashed with bcrypt before storage
  • The password field on the User schema has select: false — it is never returned in queries unless explicitly requested with .select('+password')
  • A PasswordUtils.validate() function enforces strength rules before hashing

Secret Fields

  • ApiKey.keyselect: false
  • Webhook.secretselect: false

Neither the raw API key nor the webhook signing secret is ever returned after the creation response. This is enforced at the schema level.

Refresh Token Rotation Every refresh-token call deletes the old token and issues a brand-new one — preventing replay attacks from stolen tokens.

CORS Configured to allow only the React/Vite dev origin (http://localhost:5173) with credentials: true. In production this would be parameterised via environment variable.


🗄 Database Layer

Two database clients coexist intentionally:

Mongoose handles the application layer — schemas, validation, virtuals, and query building for all CRUD operations.

DatabaseManager (raw MongoDB driver) sits below Mongoose and provides infrastructure-level capabilities that Mongoose doesn't expose:

  • Connection pooling with event monitoring — tracks connectionPoolCreated, connectionCreated, connectionClosed, and connectionPoolClosed events
  • Exponential backoff retry — up to 5 connection attempts with increasing delays (attempt × 5000ms)
  • Slow query detection — commands exceeding 1 second are logged as warnings with the command name and duration
  • Periodic health checks — pings the database admin every 30 seconds; emits a healthCheckFailed event if the ping fails
  • Metrics collection — tracks total queries, errors, and slow queries; exposes them via getMetrics()
  • executeWithRetry<T>() — wraps any async operation with per-operation retry logic (separate from connection retries)
  • Graceful disconnectdisconnect() clears the health check interval and force-closes the client

⚙️ Getting Started

Prerequisites

  • Node.js 18+
  • MongoDB 6+ (local or Atlas)
  • npm

Environment Variables

Create backened/.env:

PORT=3000
NODE_ENV=development

MONGODB_URI=mongodb://localhost:27017/task_management_system

JWT_ACCESS_SECRET=your-access-token-secret-here
JWT_REFRESH_SECRET=your-refresh-token-secret-here
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d

Install & Run

# Install backend dependencies
cd backened
npm install

# Development (hot reload)
npm run dev

# Build for production
npm run build

# Start production build
npm start

The server starts on http://localhost:3000 by default.

Frontend (Work in Progress)

The React/Vite frontend scaffolding is in frontened/. Auth pages (Login, Signup), org creation, and state management (Zustand stores for auth and org) are in place. Full UI completion is ongoing.

cd frontened
npm install
npm run dev   # http://localhost:5173

📁 Project Structure

claude-api-challange-main/
├── backened/
│   └── src/
│       ├── app.ts                  # Express app setup, CORS, route registration
│       ├── index.ts                # Entry point, DB connect, server start
│       ├── config/
│       │   ├── auth.config.ts      # JWT secrets and expiry configuration
│       │   └── database.config.ts  # Per-environment DB config (dev/prod/test)
│       ├── lib/
│       │   └── database.ts         # DatabaseManager — pooling, retry, health checks
│       ├── middleware/
│       │   └── auth.middleware.ts  # authenticate, authorize, checkOrganization
│       ├── models/
│       │   ├── enums.ts            # All domain enums (Role, TaskStatus, Priority…)
│       │   ├── index.ts            # Barrel export for all models
│       │   ├── types.ts            # All Mongoose document interfaces
│       │   └── schemas/
│       │       ├── User.ts
│       │       ├── Organizations.ts
│       │       ├── Project.ts
│       │       ├── Task.ts
│       │       ├── Team.ts
│       │       ├── TeamMember.ts
│       │       ├── TaskAssignment.ts
│       │       ├── TaskDependency.ts
│       │       ├── Comment.ts
│       │       ├── Attachment.ts
│       │       ├── ApiKey.ts
│       │       ├── RefreshToken.ts
│       │       ├── Webhook.ts
│       │       ├── WebhookDelivery.ts
│       │       └── AuditLog.ts
│       ├── modules/
│       │   ├── auth/
│       │   │   ├── auth.controller.ts
│       │   │   ├── auth.service.ts
│       │   │   └── auth.route.ts
│       │   ├── org/
│       │   │   ├── org.controller.ts
│       │   │   ├── org.service.ts
│       │   │   └── org.route.ts
│       │   └── user/
│       │       ├── user.controller.ts
│       │       ├── user.service.ts
│       │       └── user.route.ts
│       ├── types/
│       │   ├── auth.types.ts       # RegisterDTO, LoginDTO, AuthResponse, JWTPayload
│       │   ├── service-result.ts   # ServiceResult<T> generic type
│       │   └── user.types.ts
│       └── utils/
│           ├── jwt.utils.ts        # JWTUtils — sign/verify access and refresh tokens
│           └── password.utils.ts   # bcrypt hash, compare, and strength validation
└── frontened/
    └── src/
        ├── pages/                  # Home, Login, Signup, Dashboard, CreateOrg
        ├── components/             # Loading, ProtectedRoute
        ├── store/                  # Zustand: auth.js, org.js
        └── lib/                    # Axios instance with base URL + credentials

🧠 Engineering Decisions

ServiceResult<T> pattern — Services never throw errors into controllers. Instead they return { success, status, data?, error? }. Controllers check result.success and respond accordingly. This makes error handling explicit, testable, and consistent across all endpoints.

Dual database clients — Mongoose is excellent for schema management and querying; the raw MongoDB driver gives access to connection pool events and command monitoring that Mongoose abstracts away. Both are used where they're strongest.

Soft deletes everywhere — Setting deletedAt instead of calling deleteOne() means no data is ever truly lost. Audit queries, support tickets, and compliance requirements all become much easier. Hard deletion can be a scheduled cleanup job.

select: false on sensitive fields — Passwords, API keys, and webhook secrets are excluded from all queries by default at the schema level. This is a defence-in-depth measure; even if a developer forgets to exclude them in a query, they won't leak.

Refresh token rotation — Each refresh operation deletes the consumed token and creates a new one. This means stolen refresh tokens have a limited window of usefulness — the next legitimate use by the real user invalidates the attacker's copy.

TypeScript strict mode — All interfaces for Mongoose documents are explicitly defined in models/types.ts. DTOs for request bodies are typed in types/. The compiler catches mismatches before they reach production.


📬 API Testing

See backened/api-testing.md for curl examples and request/response samples for each endpoint.


Built hand-written from scratch — no generators, no scaffolding, no copy-paste. Just TypeScript, Express, MongoDB, and engineering.

About

Production-grade REST API for a multi-tenant task management system — built from scratch with TypeScript, Express 5, and MongoDB. Hand-written auth, JWT rotation, role-based access, webhooks, audit logs, and more.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors