diff --git a/README.md b/README.md index 5f69c0d..befa51d 100644 --- a/README.md +++ b/README.md @@ -1,221 +1 @@ -# Habit Tracker API - Course Material - -## Overview -A comprehensive habit tracking API built with Node.js, Express, PostgreSQL, and Drizzle ORM. This project serves as teaching material for "API Design with Node.js v5" course. - -## Tech Stack -- **Runtime**: Node.js with TypeScript -- **Framework**: Express.js -- **Database**: PostgreSQL -- **ORM**: Drizzle ORM -- **Authentication**: JWT (JSON Web Tokens) -- **Password Hashing**: bcrypt -- **Testing**: Jest & Supertest -- **Security**: Helmet & CORS - -## Features - -### Day 1 - Foundation -- RESTful API design principles -- Express routing and middleware -- PostgreSQL setup with Drizzle ORM -- CRUD operations for habits -- Error handling middleware -- Request validation with Zod - -### Day 2 - Advanced Features -- User authentication (register/login) -- JWT-based authorization -- Protected routes -- Advanced endpoints: - - **POST /habits/:id/complete** - Mark habit as completed (with duplicate prevention) - - **GET /habits/:id/stats** - Get habit statistics (streaks, completion percentage) -- Comprehensive testing suite -- Deployment preparation - -## API Endpoints - -### Authentication -- `POST /api/auth/register` - Create new user account -- `POST /api/auth/login` - Login and receive JWT token - -### User Management -- `GET /api/users/profile` - Get current user profile -- `PUT /api/users/profile` - Update user profile -- `PUT /api/users/password` - Change password - -### Habits (Protected Routes) -- `GET /api/habits` - Get all user habits -- `POST /api/habits` - Create new habit -- `PUT /api/habits/:id` - Update habit -- `DELETE /api/habits/:id` - Delete habit -- `POST /api/habits/:id/complete` - Mark habit as completed today -- `GET /api/habits/:id/stats` - Get habit statistics - -## Data Models - -### Users -```typescript -{ - id: uuid (PK) - email: string (unique) - username: string (unique) - password: string (hashed) - firstName?: string - lastName?: string - createdAt: timestamp - updatedAt: timestamp -} -``` - -### Habits -```typescript -{ - id: uuid (PK) - userId: uuid (FK -> users.id) - name: string - description?: string - frequency: 'daily' | 'weekly' | 'monthly' - targetCount: number (default: 1) - isActive: boolean (default: true) - createdAt: timestamp - updatedAt: timestamp -} -``` - -### Entries (Habit Completions) -```typescript -{ - id: uuid (PK) - habitId: uuid (FK -> habits.id) - completion_date: timestamp - note?: string - createdAt: timestamp -} -``` - -## Setup Instructions - -### Prerequisites -- Node.js 18+ -- PostgreSQL 14+ -- npm or yarn - -### Installation -```bash -# Install dependencies -npm install - -# Setup environment variables -cp .env.example .env -# Edit .env with your database credentials - -# Generate database migrations -npm run db:generate - -# Run migrations -npm run db:migrate - -# Start development server -npm run dev -``` - -### Environment Variables -```env -DATABASE_URL=postgresql://user:password@localhost:5432/habit_tracker -JWT_SECRET=your-secret-key-here -NODE_ENV=development -PORT=3000 -``` - -## Testing -```bash -# Run all tests -npm test - -# Run tests in watch mode -npm run test:watch - -# Run specific test file -npm test habits.test.ts -``` - -## Key Features Explained - -### Habit Completion Logic -- Users can only complete a habit once per day -- Attempting duplicate completion returns 409 Conflict -- Completions are tracked with timestamps for accurate streak calculation - -### Statistics Calculation -- **Current Streak**: Consecutive days completed (must include today or yesterday) -- **Longest Streak**: Maximum consecutive days ever achieved -- **Total Completions**: Count of all completion entries -- **Completion Percentage**: (Days completed / Days since creation) × 100 - -### Security Features -- Password hashing with bcrypt (10 rounds) -- JWT tokens with expiration -- Request validation with Zod schemas -- SQL injection prevention via parameterized queries -- XSS protection with Helmet -- CORS configuration for cross-origin requests - -## Course Structure - -### Day 1 Schedule -- 9:30 AM - Welcome, Intro to APIs & REST Principles -- 10:00 AM - Setting Up Node.js Project with Express -- 10:45 AM - Express Basics: Routing & Request/Response -- 11:30 AM - Middleware in Express -- 1:00 PM - Setting up Postgres & Schema Design -- 1:45 PM - Connecting Node.js to Postgres with Drizzle -- 2:30 PM - Implementing CRUD Operations (Part 1) -- 3:30 PM - Implementing CRUD Operations (Part 2) -- 4:15 PM - Error Handling Basics & Q&A - -### Day 2 Schedule -- 9:30 AM - Day 1 Recap & Day 2 Overview -- 9:45 AM - Authentication Concepts: JWT Introduction -- 10:30 AM - Implementing User Registration & Password Hashing -- 11:30 AM - Implementing User Login & JWT Issuance -- 1:00 PM - Protecting Routes with JWT Middleware -- 2:00 PM - Introduction to API Testing with Jest & Supertest -- 3:00 PM - Writing Integration Tests -- 3:45 PM - Deployment Strategies & Considerations -- 4:15 PM - Deploying the API & Final Q&A - -## Deployment Considerations -- Use environment variables for sensitive data -- Enable HTTPS in production -- Set up database connection pooling -- Configure rate limiting for public endpoints -- Implement logging and monitoring -- Use PM2 or similar for process management -- Consider containerization with Docker - -## Common Issues & Solutions - -### Database Connection Issues -- Ensure PostgreSQL is running -- Check DATABASE_URL format -- Verify database exists and user has permissions - -### Migration Errors -- Run `npm run db:generate` before `npm run db:migrate` -- Check for pending migrations with Drizzle Studio -- Ensure database schema matches TypeScript types - -### Test Failures -- Use separate test database -- Clear test data between runs -- Check for port conflicts (default: 3000) - -## Additional Resources -- [Express.js Documentation](https://expressjs.com/) -- [Drizzle ORM Documentation](https://orm.drizzle.team/) -- [JWT.io](https://jwt.io/) -- [PostgreSQL Documentation](https://www.postgresql.org/docs/) - -## License -This project is created for educational purposes as part of the API Design with Node.js v5 course. \ No newline at end of file +follow along [here](https://chrome-hamster-356.notion.site/API-Design-v5-24e05877381e8043a0c2ed09bfea5e02) diff --git a/package.json b/package.json index a7d01ee..c54ea44 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,13 @@ "description": "Habit Tracker API for Node.js course", "main": "src/index.ts", "type": "module", + "engines": { + "node": ">=24.3.0" + }, "scripts": { "dev": "node --watch src/index.ts", "start": "node src/index.ts", - "test": "vitest run", + "test": "cross-env APP_STAGE=test vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "db:generate": "drizzle-kit generate", diff --git a/src/middleware/validation.ts b/src/middleware/validation.ts index 07d4be9..583576b 100644 --- a/src/middleware/validation.ts +++ b/src/middleware/validation.ts @@ -1,5 +1,5 @@ import type { Request, Response, NextFunction } from 'express' -import { ZodSchema, ZodError } from 'zod' +import { type ZodSchema, ZodError } from 'zod' export const validateBody = (schema: ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts index ff16961..6265592 100644 --- a/src/routes/authRoutes.ts +++ b/src/routes/authRoutes.ts @@ -2,6 +2,7 @@ import { Router } from 'express' import { register, login } from '../controllers/authController.ts' import { validateBody } from '../middleware/validation.ts' import { z } from 'zod' +import { insertUserSchema } from '../db/schema.ts' const router = Router() @@ -23,7 +24,7 @@ const loginSchema = z.object({ }) // Routes -router.post('/register', validateBody(registerSchema), register) +router.post('/register', validateBody(insertUserSchema), register) router.post('/login', validateBody(loginSchema), login) export default router diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 92323a1..608a815 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -1,8 +1,8 @@ -import { SignJWT, jwtVerify, decodeJwt } from 'jose' +import { SignJWT, jwtVerify, decodeJwt, type JWTPayload } from 'jose' import { createSecretKey } from 'crypto' import env from '../../env.ts' -export interface JwtPayload { +export interface JwtPayload extends JWTPayload { id: string email: string username: string @@ -33,16 +33,3 @@ export const verifyToken = async (token: string): Promise => { username: payload.username as string, } } - -export const decodeToken = (token: string): JwtPayload | null => { - try { - const payload = decodeJwt(token) - return { - id: payload.id as string, - email: payload.email as string, - username: payload.username as string, - } - } catch { - return null - } -} diff --git a/tests/setup/globalSetup.ts b/tests/setup/globalSetup.ts index 7d8c464..2237647 100644 --- a/tests/setup/globalSetup.ts +++ b/tests/setup/globalSetup.ts @@ -1,5 +1,5 @@ import { db } from '../../src/db/connection.ts' -import { users, habits, entries } from '../../src/db/schema.ts' +import { users, habits, entries, tags, habitTags } from '../../src/db/schema.ts' import { sql } from 'drizzle-orm' import { execSync } from 'child_process' @@ -11,6 +11,8 @@ export default async function setup() { await db.execute(sql`DROP TABLE IF EXISTS ${entries} CASCADE`) await db.execute(sql`DROP TABLE IF EXISTS ${habits} CASCADE`) await db.execute(sql`DROP TABLE IF EXISTS ${users} CASCADE`) + await db.execute(sql`DROP TABLE IF EXISTS ${tags} CASCADE`) + await db.execute(sql`DROP TABLE IF EXISTS ${habitTags} CASCADE`) // Use drizzle-kit CLI to push schema to database console.log('🚀 Pushing schema using drizzle-kit...')