A REST API for blog publishing with admin approval workflow, built with Node.js, Express, and PostgreSQL.
- User Management: Registration, authentication, role-based access control
- Blog Post Management: CRUD operations with approval workflow
- Admin Approval: Posts require admin approval before publication
- Category System: Hierarchical categories with parent-child relationships
- Comment System: Nested comments with approval workflow
- API Key Management: Secure API access with key-based authentication
- File Upload: Cloudinary integration for banner images and user profiles
- Rate Limiting: Protection against API abuse
- Runtime: Node.js (ES Modules)
- Framework: Express.js 5.x
- Database: PostgreSQL
- ORM: Prisma 6.x
- Authentication: JWT + Refresh Tokens
- Security: bcryptjs, express-validator, express-rate-limit
- File Upload: Multer + Cloudinary
- Node.js 18+
- PostgreSQL 12+
- Docker (optional)
- Cloudinary account
-
Clone & Install
git clone <repository-url> cd inkwell npm install
-
Environment Setup
cp .env.example .env # Edit .env with your configuration
-
Database Setup
docker-compose up -d npx prisma migrate dev npx prisma generate
-
Start Application
npm run dev
# Server
PORT=3000
NODE_ENV=development
# Database
DATABASE_URL="postgresql://<username>:<password>@<localhost>:5432/inkwell"
# JWT Security
JWT_SECRET=your-super-secret-jwt-key
JWT_REFRESH_SECRET=your-super-secret-refresh-key
JWT_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d
# CORS
CORS_ORIGIN=http://localhost:3000 # Frontend URL
# Cloudinary
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
public/
├── uploads/
│ ├── posts/ # Blog post banner images
│ └── userProfiles/ # User profile pictures
├── temp/ # Temporary upload storage
└── .gitkeep
- Post Images: 5MB max (JPEG, PNG, WebP)
- User Profile: 2MB max (JPEG, PNG, WebP)
- General: 10MB max (JPEG, PNG, WebP)
Request:
{
"username": "johndoe",
"email": "john@example.com",
"password": "securepassword123",
"fullName": "John Doe"
}
Response:
{
"success": true,
"message": "User registered successfully",
"data": {
"user": {
"id": "uuid",
"username": "johndoe",
"email": "john@example.com",
"fullName": "John Doe",
"role": "USER"
}
}
}
Request:
{
"username": "johndoe" OR "email" : "johndoe@gmail.com",
"password": "securepassword123"
}
Response:
{
"success": true,
"message": "Login successful",
"data": {
"accessToken": "jwt-token",
"refreshToken": "refresh-token",
"user": {
"id": "uuid",
"username": "johndoe",
"role": "USER"
}
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"endedAt": "2024-12-31T23:59:59.000Z"
}
Response:
{
"success": true,
"message": "API key generated successfully",
"data": {
"apiKey": "generated-api-key"
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Response:
{
"success": true,
"data": {
"user": {
"id": "uuid",
"username": "johndoe",
"email": "john@example.com",
"fullName": "John Doe",
"role": "USER",
"profileImage": "url"
}
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"title": "My First Blog Post",
"description": "This is the content of my blog post",
"categories": ["technology", "programming"]
}
Response:
{
"success": true,
"message": "Post created successfully",
"data": {
"post": {
"id": "uuid",
"title": "My First Blog Post",
"status": "PENDING",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
}
Response:
{
"success": true,
"data": {
"posts": [
{
"id": "uuid",
"title": "My First Blog Post",
"status": "APPROVED",
"author": {
"username": "johndoe"
},
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Response:
{
"success": true,
"data": {
"post": {
"id": "uuid",
"title": "My First Blog Post",
"description": "Content...",
"status": "APPROVED",
"slug": "my-first-blog-post",
"bannerImage": "url",
"author": {
"username": "johndoe"
},
"categories": ["technology"],
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"message": "Great article!",
"parentId": null
}
Response:
{
"success": true,
"message": "Comment created successfully",
"data": {
"comment": {
"id": "uuid",
"message": "Great article!",
"status": "PENDING",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}
}
Response:
{
"success": true,
"data": {
"comments": [
{
"id": "uuid",
"message": "Great article!",
"status": "APPROVED",
"author": {
"username": "johndoe"
},
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"categoryName": "Technology",
"parentId": null
}
Response:
{
"success": true,
"message": "Category created successfully",
"data": {
"category": {
"id": "uuid",
"categoryName": "Technology",
"parentId": null
}
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Response:
{
"success": true,
"data": {
"pendingPosts": [
{
"id": "uuid",
"title": "My First Blog Post",
"status": "PENDING",
"author": {
"username": "johndoe"
},
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"comment": "Great content, approved!",
"rating": 5
}
Response:
{
"success": true,
"message": "Post approved successfully",
"data": {
"post": {
"id": "uuid",
"status": "APPROVED"
}
}
}
Headers: Authorization: Bearer <jwt-token>
, X-API-Key: <api-key>
Request:
{
"comment": "Content needs improvement"
}
Response:
{
"success": true,
"message": "Post rejected successfully",
"data": {
"post": {
"id": "uuid",
"status": "REJECTED"
}
}
}
Response:
{
"success": true,
"message": "API is running",
"timestamp": "2024-01-01T00:00:00.000Z"
}
- JWT Tokens: Access token (15min) + Refresh token (7 days)
- API Keys: Required for all protected endpoints
- Password Hashing: bcryptjs with salt rounds
- Role-Based Access: USER vs ADMIN permissions
- Resource Ownership: Users can only modify their own content
- Admin Privileges: Post approval, category management
- Window: 15 minutes
- Limit: 100 requests per window
- Headers:
X-RateLimit-*
for monitoring
- Request Validation: express-validator middleware
- SQL Injection Protection: Prisma ORM
- XSS Protection: Input sanitization
- File Type Validation: Only JPEG, PNG, WebP allowed
- Size Limits: Configurable per upload type
- Cloud Storage: Secure Cloudinary integration
- Automatic Cleanup: Temporary files removed after upload
- User: Authentication, roles, profile
- Post: Blog content with approval workflow
- Category: Hierarchical organization
- Comment: Nested discussion system
- PostReview: Admin approval workflow
- ApiKey: Secure API access
- UUID primary keys
- Timestamp tracking
- Soft delete support
- Referential integrity
npm run dev # Development with nodemon
npm start # Production server
npx prisma generate # Generate client
npx prisma migrate dev # Run migrations
npx prisma studio # Database GUI
docker-compose up -d # Start PostgreSQL
ISC License
Sumit Singh Tomar
Inkwell - Blog Publishing API with Admin Approval Flow 🚀