Full-featured Twitter-like API built with TypeScript, Express, and PostgreSQL
Features β’ Quick Start β’ API Docs β’ Architecture β’ Contributing
Chirpy is a production-ready micro-blogging API that demonstrates modern backend development practices. Built with a clean, layered architecture, it provides all the essential features for a Twitter-like social platform including user authentication, post management, and premium subscriptions.
- ποΈ Clean Architecture: Layered design with clear separation of concerns
- π Secure by Default: JWT auth, Argon2 password hashing, refresh tokens
- π Type-Safe: Written in TypeScript with strict type checking
- π Production-Ready: Comprehensive error handling, logging, and validation
- π Well-Documented: Extensive inline docs and API reference
- π§ͺ Tested: Unit tests with Vitest
- π― Best Practices: Follows SOLID principles and Express.js conventions
-
π€ User Management
- User registration with email/password
- Secure authentication with JWT tokens
- Refresh token rotation
- Profile updates
-
π Chirp Management
- Create chirps (max 140 characters)
- Read all chirps or filter by author
- Sort chronologically (ascending/descending)
- Delete own chirps
- Automatic profanity filtering
-
π Security
- JWT-based authentication
- Argon2 password hashing
- Refresh token revocation
- API key authentication for webhooks
- Resource ownership authorization
- SQL injection protection (Drizzle ORM)
-
π Premium Features
- ChirpyRed premium subscriptions
- Webhook integration for payment processing
- Premium status tracking
-
π Admin Tools
- Metrics dashboard
- Hit counter
- Reset functionality (dev mode only)
- Node.js 21.7.0 or higher
- PostgreSQL 14+
- npm or yarn
-
Clone the repository
git clone https://github.com/ahmedk20/chirpy.git cd chirpy -
Install dependencies
npm install
-
Set up environment variables
cp .env.example .env
Edit
.envwith your configuration:DB_URL=postgres://username:password@localhost:5432/chirpy?sslmode=disable JWT_SECRET=your-super-secret-jwt-key-min-32-chars PLATFORM=dev POLKA_KEY=your-webhook-api-key
-
Set up the database
# Create the database createdb chirpy # Generate and run migrations npm run db:generate npm run db:migrate
-
Build and start
npm run build npm start
π Server running at
http://localhost:3000
# Run with auto-compilation
npm run dev
# Run TypeScript in watch mode
npm run watchnpm testchirpy/
βββ public/ # Static files
βββ src/
β βββ config/ # Configuration & constants
β βββ controllers/ # HTTP request handlers
β βββ middleware/ # Express middleware
β βββ services/ # Business logic layer
β βββ db/ # Database layer (Drizzle ORM)
β βββ routes/ # API route definitions
β βββ types/ # TypeScript type definitions
β βββ utils/ # Utility functions
β βββ tests/ # Test files
βββ .env.example # Environment variables template
βββ drizzle.config.ts # Drizzle ORM configuration
βββ tsconfig.json # TypeScript configuration
βββ package.json # Project dependencies
POST /api/users
Content-Type: application/json
{
"email": "user@example.com",
"password": "securePassword123"
}Response: 201 Created
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"isChirpyRed": false,
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}POST /api/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securePassword123",
"expiresInSeconds": 3600
}Response: 200 OK
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "a1b2c3d4e5f6...",
"isChirpyRed": false,
"createdAt": "2025-01-15T10:30:00.000Z",
"updatedAt": "2025-01-15T10:30:00.000Z"
}POST /api/refresh
Authorization: Bearer <refresh-token>Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/revoke
Authorization: Bearer <access-token>Response: 204 No Content
POST /api/chirps
Authorization: Bearer <access-token>
Content-Type: application/json
{
"body": "Hello, world! This is my first chirp! π¦"
}Response: 201 Created
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"body": "Hello, world! This is my first chirp! π¦",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2025-01-15T10:35:00.000Z",
"updatedAt": "2025-01-15T10:35:00.000Z"
}GET /api/chirps?sort=desc&authorId=<user-id>Query Parameters:
sort(optional):ascordesc(default:asc)authorId(optional): Filter by user ID
Response: 200 OK
[
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"body": "Hello, world! This is my first chirp! π¦",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2025-01-15T10:35:00.000Z",
"updatedAt": "2025-01-15T10:35:00.000Z"
}
]GET /api/chirps/:chirpIDResponse: 200 OK
DELETE /api/chirps/:chirpID
Authorization: Bearer <access-token>Response: 204 No Content
β οΈ Note: Users can only delete their own chirps
POST /api/validate_chirp
Content-Type: application/json
{
"body": "This chirp contains kerfuffle!"
}Response: 200 OK
{
"cleanedBody": "This chirp contains ****!"
}PUT /api/users
Authorization: Bearer <access-token>
Content-Type: application/json
{
"email": "newemail@example.com",
"password": "newSecurePassword123"
}Response: 200 OK
GET /api/metricsResponse: 200 OK (plain text)
Hits: 42
GET /admin/metricsResponse: 200 OK (HTML page)
POST /admin/resetResponse: 200 OK (plain text)
β οΈ Note: Only available whenPLATFORM=dev
POST /api/polka/webhooks
Authorization: ApiKey <polka-api-key>
Content-Type: application/json
{
"event": "user.upgraded",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000"
}
}Response: 204 No Content
GET /api/healthzResponse: 200 OK (plain text)
OK
Chirpy follows a layered architecture pattern for maximum maintainability and testability.
βββββββββββββββββββββββββββββββββββββββββββ
β HTTP Request β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββββββββ
β Routes (API Endpoints) β
β - Define endpoints β
β - Apply middleware β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββββββββ
β Middleware (Cross-cutting) β
β - Authentication (JWT) β
β - Authorization (API Key) β
β - Logging β
β - Error handling β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββββββββ
β Controllers (HTTP Handlers) β
β - Extract request data β
β - Call services β
β - Format responses β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββββββββ
β Services (Business Logic) β
β - Validation β
β - Business rules β
β - Coordinate operations β
βββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββββββββ
β Database (Data Access) β
β - Queries (Drizzle ORM) β
β - Schema definitions β
β - Migrations β
βββββββββββββββββββββββββββββββββββββββββββ
- Dependency Injection: Services injected into controllers
- Repository Pattern: Database queries abstracted in
db/queries.ts - Service Layer Pattern: Business logic separated from HTTP logic
- Factory Pattern: Singleton services (metrics, chirp validation)
- Middleware Pattern: Cross-cutting concerns (auth, logging, errors)
- User registers or logs in with email/password
- Password hashed with Argon2 (memory-hard algorithm)
- Server generates:
- Access Token (JWT, 1 hour expiry)
- Refresh Token (random hex, 60 days expiry, stored in DB)
- Client sends access token in
Authorization: Bearer <token>header - Middleware validates token and extracts
userId - When access token expires, client uses refresh token to get new access token
- Refresh tokens can be revoked
β Password Security
- Argon2 hashing (winner of Password Hashing Competition)
- Minimum 8 character requirement
- Passwords never stored in plain text
β Token Security
- Short-lived access tokens (1 hour)
- Refresh token rotation
- Token revocation support
- Secure random token generation
β API Security
- JWT validation on protected routes
- API key authentication for webhooks
- Resource ownership verification
- SQL injection protection (parameterized queries)
- Input validation and sanitization
β Data Security
- Environment variable configuration
- Secrets not committed to git
- Centralized error handling (no data leaks)
βββββββββββββββββββββββ
β Users β
βββββββββββββββββββββββ€
β id (PK) β
β email (unique) β
β hashedPassword β
β isChirpyRed β
β createdAt β
β updatedAt β
ββββββββββββ¬βββββββββββ
β
β 1:N
β
ββββββββββββΌβββββββββββ βββββββββββββββββββββββ
β Chirps β β Refresh Tokens β
βββββββββββββββββββββββ€ βββββββββββββββββββββββ€
β id (PK) β β token (PK) β
β body β β userId (FK) βββββββ
β userId (FK) β β expiresAt β β
β createdAt β β revokedAt β β
β updatedAt β β createdAt β β
βββββββββββββββββββββββ β updatedAt β β
βββββββββββββββββββββββ β
N:1 ββββββββββββββ
Users
- Stores user accounts
- Email must be unique
- Password hashed with Argon2
- Premium status tracked via
isChirpyRed
Chirps
- User-generated posts (max 140 chars)
- Foreign key to user (cascade delete)
- Timestamps for sorting
Refresh Tokens
- Secure refresh tokens for auth
- Can be revoked
- Automatically deleted when user is deleted
| Variable | Required | Default | Description |
|---|---|---|---|
DB_URL |
β Yes | - | PostgreSQL connection string |
JWT_SECRET |
β Yes | - | Secret key for JWT signing (min 32 chars) |
PLATFORM |
No | prod |
dev or prod (affects admin endpoints) |
POLKA_KEY |
β Yes | - | API key for webhook authentication |
Edit src/config/constants.ts to modify:
PORT = 3000 // Server port
MAX_CHIRP_LENGTH = 140 // Character limit
PROFANE_WORDS = [...] // Words to censor
REFRESH_TOKEN_EXPIRY_DAYS = 60 // Token validity
DEFAULT_JWT_EXPIRY_SECONDS = 3600 // Access token TTL# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coveragesrc/tests/
βββ auth.test.ts # JWT creation and validation
βββ chirp.test.ts # Chirp validation and filtering
βββ user.test.ts # User creation and updates
| Command | Description |
|---|---|
npm run build |
Compile TypeScript to JavaScript |
npm start |
Start production server |
npm run dev |
Run in development mode |
npm run watch |
Watch mode (auto-compile) |
npm test |
Run tests |
npm run db:generate |
Generate database migrations |
npm run db:migrate |
Run migrations |
npm run db:studio |
Open Drizzle Studio (DB GUI) |
Build fails with TypeScript errors
# Clean and rebuild
rm -rf dist/
npm run buildDatabase connection fails
- Verify PostgreSQL is running:
pg_isready - Check
DB_URLformat in.env - Ensure database exists:
createdb chirpy
JWT errors in production
- Ensure
JWT_SECRETis set and at least 32 characters - Verify tokens haven't expired
- Check system clock synchronization
Migration errors
# Reset migrations
npm run db:generate
npm run db:migrateContributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch
git checkout -b feature/amazing-feature
- Make your changes
- Follow existing code style
- Add tests for new features
- Update documentation
- Commit your changes
git commit -m 'Add amazing feature' - Push to your fork
git push origin feature/amazing-feature
- Open a Pull Request
- Use TypeScript strict mode
- Follow existing naming conventions
- Add JSDoc comments for public APIs
- Keep functions small and focused
- Write tests for new features
Follow Conventional Commits:
feat:New featurefix:Bug fixdocs:Documentation changesrefactor:Code refactoringtest:Test updateschore:Build process or tooling changes
This project is licensed under the ISC License - see the LICENSE file for details.
Yomna Khaled
- GitHub: @ahmedk20
- Email: ak19111911@gmail.com
- Express.js - Web framework
- Drizzle ORM - Type-safe ORM
- Argon2 - Password hashing
- JWT - Authentication standard
- TypeScript - Type safety
- Language: TypeScript
- Framework: Express.js 5.x
- Database: PostgreSQL + Drizzle ORM
- Auth: JWT + Refresh Tokens
- Testing: Vitest
- Code Style: ESLint + Prettier
- License: ISC
- Add email verification
- Implement rate limiting
- Add image upload support
- Create REST API documentation (OpenAPI/Swagger)
- Add real-time notifications (WebSockets)
- Implement follower/following system
- Add direct messaging
- Create admin panel UI
- Add comprehensive integration tests
- Set up CI/CD pipeline
- Docker containerization
- Add API versioning
β Star this repo if you find it helpful!
Made with β€οΈ and TypeScript