A developer social community REST API built with NestJS · TypeScript · MongoDB · Redis · BullMQ
- Overview
- Tech Stack
- Architecture
- Project Structure
- API Endpoints
- Data Models
- Core Business Logic
- External Integrations
- Configuration & Environment Variables
- Running Locally
- Running Tests
- Known Issues & Risks
Dev Community is a backend API for a developer-focused social platform where developers can:
- Register and authenticate with JWT
- Publish posts (time-gated to 9 AM – 10 PM Asia/Dhaka)
- Comment and reply on posts
- React (like/dislike) to posts, comments, and replies
- Receive email notifications on key events (signup, comments, excessive dislikes)
- Browse a ranked feed sorted by engagement (reactions + comments)
A Swagger UI is available at http://localhost:3000/api after startup.
| Layer | Technology |
|---|---|
| Language | TypeScript 5.7 |
| Runtime | Node.js |
| Framework | NestJS 11 |
| HTTP Adapter | Express (@nestjs/platform-express) |
| Database | MongoDB via Mongoose 8 |
| Auth | JWT (@nestjs/jwt) + bcrypt |
| Job Queue | BullMQ + Redis |
| Caching | @nestjs/cache-manager (Redis-backed) |
| Redis Client | ioredis + @nestjs-modules/ioredis |
| Scheduling | @nestjs/schedule (cron jobs) |
@nestjs-modules/mailer + Nodemailer (Gmail SMTP) |
|
| Validation | class-validator + class-transformer |
| API Docs | Swagger (@nestjs/swagger) |
Single-process layered monolith following standard NestJS module conventions:
HTTP Request
└── Controller (routing, request/response shaping)
└── Guard(s) (JWT auth · rate-limit · posting-window)
└── Service (business logic)
└── Model (MongoDB via Mongoose)
Background work (email sending) is decoupled via a BullMQ queue. The main process both enqueues jobs (via MailService) and processes them (via MailProcessor) — there is no separate worker binary.
┌─────────────┐ HTTP ┌──────────────────┐
│ Client │ ─────────────▶ │ NestJS API │
└─────────────┘ │ (port 3000) │
└────────┬─────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌───────────┐
│ MongoDB │ │ Redis │ │ BullMQ │
│ dev_comm│ │ :6379 │ │ mail queue│
└─────────┘ └──────────┘ └─────┬─────┘
│
┌─────▼──────┐
│MailProcessor│
│ (in-process)│
└─────┬───────┘
│
┌─────▼───────┐
│ Gmail SMTP │
└─────────────┘
src/
├── main.ts # Application entry point
├── app.module.ts # Root module — wires everything together
│
├── auth/ # Registration, login, JWT, refresh tokens
│ ├── Dtos/ # createUserDto · loginUserDto · refreshTokenDto
│ ├── Schemas/refreshToken.entity.ts
│ └── guards/auth.guard.ts # JWT Bearer guard
│
├── user/ # User profile read/update/delete
│ ├── Dtos/updateUserDto.ts
│ └── Schemas/user.entity.ts
│
├── post/ # Post CRUD + aggregated reads + ranked feed
│ ├── Dtos/
│ ├── Schemas/post.entity.ts
│ ├── post-aggregate.interface.ts
│ └── posting-window/ # Time-gated posting (cron + guard)
│ └── guards/
│ ├── postingWindow.guard.ts
│ └── rateLimit.guard.ts
│
├── comment/ # Comment CRUD + BullMQ notification dispatch
│ ├── Dtos/
│ └── schema/comment.entity.ts
│
├── reply/ # Reply CRUD
│ ├── Dtos/
│ └── Schemas/reply.entity.ts
│
├── reaction/ # Toggle like/dislike on Post | Comment | Reply
│ ├── Dtos/
│ ├── Schemas/reaction.entity.ts
│ └── reaction-type.enum.ts
│
├── mail/ # BullMQ queue + email processor
│ ├── mail.module.ts
│ ├── mail.service.ts # Enqueues jobs
│ └── mail.processor.ts # Consumes jobs & sends emails
│
├── common/
│ ├── decorators/rate-limit.decorator.ts
│ └── redis/redis.module.ts # Global ioredis connection
│
├── config/config.ts # Maps env vars to nested config keys
└── interfaces/
└── jwt-payload.interface.ts
All protected endpoints require Authorization: Bearer <access_token>.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/signup |
❌ | Register a new developer |
| POST | /auth/login |
❌ | Login, returns accessToken + refreshToken |
| POST | /auth/refresh |
✅ | Refresh tokens using a valid refresh token |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /user |
✅ | List all developers |
| GET | /user/:id |
✅ | Get a developer by ID |
| PATCH | /user/:id |
✅ (owner) | Update skill & experience |
| DELETE | /user/:id |
✅ (owner) | Delete own account |
| Method | Path | Auth | Guards | Description |
|---|---|---|---|---|
| GET | /post |
✅ | — | All posts (fully aggregated with comments/replies/reactions) |
| GET | /post/ranked |
✅ | — | Top N posts by engagement score (cached 30s) |
| GET | /post/:id |
✅ | — | Single post (fully aggregated) |
| POST | /post |
✅ | PostingWindow · RateLimit | Create a post (max 5/hr, 9AM–10PM only) |
| PATCH | /post/:id |
✅ (owner) | — | Update post title/description |
| DELETE | /post/:id |
✅ (owner) | — | Delete a post |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /comment |
✅ | All comments |
| GET | /comment/:id |
✅ | Single comment |
| POST | /comment |
✅ | Create a comment (triggers email notification) |
| PATCH | /comment/:id |
✅ (owner) | Update comment |
| DELETE | /comment/:id |
✅ (owner) | Delete comment |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /reply |
✅ | All replies |
| GET | /reply/:id |
✅ | Single reply |
| POST | /reply |
✅ | Create a reply |
| DELETE | /reply/:id |
✅ (owner) | Delete a reply |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /reaction |
✅ | Toggle like/dislike on Post, Comment, or Reply |
| GET | /reaction/:targetId |
❌ | List all reactions for a target |
| GET | /reaction/:targetId/count |
❌ | Count likes & dislikes for a target |
| Field | Type | Notes |
|---|---|---|
name |
String | required |
username |
String | required, unique |
email |
String | required, unique |
password |
String | bcrypt hash, never returned |
skill |
String | required |
experience |
String | required |
createdAt, updatedAt |
Date | auto (timestamps) |
| Field | Type | Notes |
|---|---|---|
postTitle |
String | required |
postDescription |
String | required |
user |
ObjectId → User | required |
createdAt, updatedAt |
Date | auto |
| Field | Type | Notes |
|---|---|---|
commentTitle |
String | required |
commentDescription |
String | required |
user |
ObjectId → User | required |
post |
ObjectId → Post | required |
reply |
ObjectId[] → Reply | default [] |
createdAt, updatedAt |
Date | auto |
| Field | Type | Notes |
|---|---|---|
replyDescription |
String | required |
user |
ObjectId → User | required |
comment |
ObjectId → Comment | required |
reaction |
ObjectId[] → Reaction | default [] |
createdAt, updatedAt |
Date | auto |
| Field | Type | Notes |
|---|---|---|
user |
ObjectId → User | required |
type |
'like' | 'dislike' |
required |
target |
ObjectId (polymorphic) | required |
onModel |
'Post' | 'Comment' | 'Reply' |
required, drives refPath |
createdAt, updatedAt |
Date | auto |
| Field | Type | Notes |
|---|---|---|
token |
String | UUID v4 |
userId |
ObjectId | required |
expiryDate |
Date | +3 days from creation |
POST /auth/signup→ validate → check email uniqueness → bcrypt hash → create user → send welcome email (via queue)POST /auth/login→ find by email → bcrypt compare → issue JWT (2h) + refresh token (3 days) stored in DBPOST /auth/refresh→ find & delete refresh token from DB → issue new pair
Posts can only be created between 9:00 AM and 10:00 PM (Asia/Dhaka):
- A cron job at
0 9 * * *callsPostingWindowService.enable() - A cron job at
0 22 * * *callsPostingWindowService.disable() PostingWindowGuardblocks creation when the service reports disabled
Additionally, a rate limit of 5 posts per hour per user is enforced via Redis INCR.
POST /reaction is idempotent and toggles state:
- No existing reaction → add it
- Same type already exists → remove it (un-like)
- Different type exists → switch it (like ↔ dislike)
GET /post/ranked runs a MongoDB aggregation that computes:
rankingScore = totalReactions + totalComments
Results are sorted descending and cached in Redis for 30 seconds.
| Trigger | Job Name | Email Subject |
|---|---|---|
| User signup | user-created-email |
Welcome to Dev Community! |
| Comment on post | comment-notification-email |
New comment notification |
| 10 dislikes reached | ten-dislikes-email |
Your {type} received 10 dislikes |
All jobs use exponential backoff with 3 retry attempts.
Email is sent via Gmail using an App Password (not your Google account password). Configure EMAIL_USER and EMAIL_PASS in .env.
Used for three purposes simultaneously:
- BullMQ transport — async mail job queue
- Rate limiting — per-user, per-action INCR/EXPIRE counters
- Response caching — ranked posts result (30s TTL)
All connect to the same local Redis instance.
Available at http://localhost:3000/api — all routes are documented with @ApiOperation and @ApiProperty decorators.
Create a .env file in the project root with the following variables:
# JWT signing secret (use a long random string in production)
JWT_SECRET=your_secret_key_here
# MongoDB connection string
DB_URI=mongodb://localhost/dev_comm
# Gmail account for sending emails
EMAIL_USER=you@gmail.com
# Gmail App Password (Settings > Security > App Passwords)
EMAIL_PASS=xxxx xxxx xxxx xxxx
# Application host and port
HOST=127.0.0.1
PORT=3000
# Redis port (currently informational — connections are hardcoded to 127.0.0.1:6379)
REDIS_PORT=6379Note:
.envis gitignored and must be created manually. Never commit real credentials.
| Requirement | Version / Notes |
|---|---|
| Node.js | ≥ 18 |
| MongoDB | Running on localhost:27017 |
| Redis | Running on localhost:6379 |
| Gmail App Password | Required for email features |
# Start Redis via Docker
docker run -d -p 6379:6379 redis# 1. Install dependencies
npm install
# 2. Create your .env file (see section above)
# 3. Start in development (watch mode — recommended)
npm run start:dev
# 4. Open Swagger UI
# http://localhost:3000/api# Standard start
npm run start
# Production (requires build first)
npm run build
npm run start:prod# Unit tests
npm run test
# Watch mode
npm run test:watch
# End-to-end tests
npm run test:e2e
# Coverage report
npm run test:covCommentService enqueues a job named 'comment-notification', but MailProcessor only handles 'comment-notification-email'. The job name mismatch means the processor throws Unhandled job type and no email is ever sent. Additionally, the payload shapes don't match — the enqueued job sends { postOwnerId, commenterId, commentId } but the handler expects { email, message }.
Several controllers import error from Node's console module and throw it as though it were an exception class. This produces unhandled errors instead of proper HTTP 4xx responses. Affects PostController, UserController, CommentController, and ReplyController.
// ❌ Wrong — `error` is console.error, not an exception class
import { error } from 'console';
throw new error('Invalid User');
// ✅ Correct
throw new UnauthorizedException('Invalid User');PostingWindowService stores the enabled/disabled flag as a plain boolean. On server restart, posting defaults to disabled until the next 9 AM cron fires — even if the current time is within the window. The fix would be to persist the state in Redis (the module is already imported but unused for this purpose).
127.0.0.1:6379 is hardcoded in three places (app.module.ts, redis.module.ts, posting-window.module.ts). The REDIS_PORT env var in .env has no effect on any of these connections.
The method and its processor handler are fully implemented, but no service ever calls sendTenDislikesEmail(). The 10-dislikes feature is wired but not connected.
CacheModule is configured with ttl: 0, meaning entries never expire unless explicitly set. Only the ranked-posts endpoint sets its own 30s TTL. Any future cached routes relying on the global default will cache indefinitely.
There is no example env file in the repository. Use the Configuration section above as the reference.
MIT