A reusable Node.js backend template with authentication flow, user + subscription + avatar management, email verification, password reset, notifications (event bus + websockets) and API docs.
I built this so I donβt need to rewrite the same boring auth + profile stuff every project π. Kept it simple but tried to stay mindful about basic security.
- PostgreSQL (Neon hosting)
- Express + NodeJS
- JWT (access + refresh)
- Google Login (idToken flow)
- Mailtrap (email sending)
- Cloudinary (avatar storage + basic transform) with multer memoryStorage
- Socket.io (real-time notifications)
- Swagger (API docs)
- Helmet + Rate limit + CORS
- Morgan for better logging
When I start a fullβstack project usually I have to π«π«:
- Register / login / email verify
- Password reset / change
- Manage profile + avatar
- Basic subscription tiers
- Event β notification β socket chain
- Docs + rate limiting
So this template covers those into something I can plug in fast and extend.
- Copy
src/.env.exampleto.env(leave insrcat the root but never commit the real one) - Fill values (DB URL, JWT secrets, etc.)
- Install + start:
npm install
npm run start- (First time) create table:
GET /api/auth/init-table
- Visit API docs:
http://localhost:3000/api/docs
JSON spec:http://localhost:3000/api/docs.json
- Local username/email + password auth
- Optional email verification (
REQUIRE_EMAIL_VERIFICATION) - Google OAuth login (idToken POST flow)
- Password reset (token + expiry)
- Account lock after repeated failed logins
- Subscription type update (free | plus | premium)
- Avatar upload (memory β Cloudinary with face crop + resize)
- Event bus β Notification service β Socket.io emit
- Swagger docs generation
- Rate limiting (general vs login)
- Basic logging with redaction of sensitive fields by Morgan
src/
index.js # Entry point
routes/ # authRoutes.js, userRoutes.js
controllers/ # userController.js (big brain)
models/ # userModel.js (Postgres queries)
middlewares/ # authenticateToken.js (attaches user info with the requset headers)
utils/ # emailUtils, cloudinary helper
events/ # eventBus.js + eventsNames.js
notifications/ # notificationService.js
realtime/ # socketServer.js
docs/ # swaggerConfig.js
- Register β (maybe email verify)
- Login β returns
accessToken+refreshToken(refresh stored in DB plain for now) - Frontend saves access token (currently localStorage style) β calls
/api/auth/verify-tokenon load - Logout β clears stored refresh token
- Password reset β token emailed (Mailtrap) β confirm reset
- Google login β verify idToken β create or link account
Simple column: subscription_type with enum values: free | plus | premium.
Update endpoint:
PATCH /api/user/subscription/:userId
{ "subscription_type": "plus" }
ππΌ (You can later hook Stripe/Paddle webhooks to set this server-side.)
Flow:
POST /api/user/avatar/:userIdwith multipart fieldavatar- Multer keeps it in memory (no disk clutter)
- Piped to Cloudinary with:
- Resize square 400x400
- Face crop if detectable
- Auto quality / format
- DB updated with
avatar_url - Event emitted β socket broadcast (if you wire client side)
ππΌ Old images are not deleted right now (could store
public_idand remove previous).
Pieces:
eventBus(Node EventEmitter) β pub(publisher) inside processeventsNames.jsβ keeps constants (e.g.USER_LOGIN,USER_PROFILE_UPDATED,PASSWORD_RESET_REQUESTED)- Controllers emit events after important actions
notificationService.jslistens to those events and decides what to do (right now: emit over socket)socketServer.jsattaches to the HTTP server and authenticates on connection (JWT at handshake)
Example path:
User updates profile
β userController.updateProfile()
β bus.emit(Events.USER_PROFILE_UPDATED, { userId, changed: [...] })
β notificationService catches it
β emits through socket.io to room "user:<id>" (extend this)
β frontend listens β updates UI live
Future ideas:
- Persist notifications in DB
- Add unread/read states
- Swap EventEmitter with Redis Pub/Sub for multi-instance scaling
- Add email fallback for critical events
Full details already documented with Swagger:
Auth:
GET /api/auth/init-table
POST /api/auth/register
POST /api/auth/login
POST /api/auth/google-login
POST /api/auth/send-verification-email
GET /api/auth/verify-email?token=...
GET /api/auth/verify-token
POST /api/auth/logout/:userId
POST /api/auth/password/request
POST /api/auth/password/reset
POST /api/auth/password/change
User:
GET /api/user/get-profile/:userId
PATCH /api/user/update-profile/:userId
PATCH /api/user/subscription/:userId
POST /api/user/avatar/:userId
Go to /api/docs for schemas + payload examples.
GET /api/auth/init-table
Headers: (none)
Creates users table if missing (idempotent). Use once at start. Response:
{ "success": true, "message": "users table ready" }POST /api/auth/register
Content-Type: application/json
Body:
{
"username": "catman",
"email": "cat@meow.com",
"password": "secret123"
}Response (verification required):
{
"success": true,
"message": "User registered. Please verify email.",
"user": {
"id": 1,
"username": "catman",
"email": "cat@meow.com",
"email_verified": false,
"subscription_type": "free",
"requires_verification": true
}
}POST /api/auth/login
Content-Type: application/json
Body (identifier can be username or email):
{ "identifier": "catman", "password": "secret123" }Success:
{
"success": true,
"tokens": {
"accessToken": "JWT_ACCESS",
"refreshToken": "JWT_REFRESH"
},
"user": {
"id": 1,
"username": "catman",
"email": "cat@meow.com",
"subscription_type": "free"
}
}Fail (wrong creds):
{ "success": false, "error": "Invalid credentials" }POST /api/auth/google-login
Content-Type: application/json
Body:
{ "idToken": "GOOGLE_ID_TOKEN" }Response similar to login (creates user if new).
POST /api/auth/send-verification-email
Content-Type: application/json
Body:
{ "email": "cat@meow.com" }Always returns success (unless spam limited):
{ "success": true, "message": "If not verified, verification email sent" }GET /api/auth/verify-email?token=VERIFICATION_TOKEN
Success:
{ "success": true, "message": "Email verified" }Fail:
{ "success": false, "error": "Invalid or expired token" }GET /api/auth/verify-token
Authorization: Bearer <accessToken>
Success:
{ "success": true, "user": { "id": 1, "username": "catman", "subscription_type": "free" } }401 if token bad/expired.
POST /api/auth/logout/1
Authorization: Bearer <accessToken>
Clears stored refresh token.
{ "success": true, "message": "Logged out" }POST /api/auth/password/request
Content-Type: application/json
Body:
{ "email": "cat@meow.com" }Always generic:
{ "success": true, "message": "If account exists, reset email sent" }POST /api/auth/password/reset
Content-Type: application/json
Body:
{ "token": "RESET_TOKEN", "newPassword": "newSecret123" }Success:
{ "success": true, "message": "Password updated" }POST /api/auth/password/change
Authorization: Bearer <accessToken>
Content-Type: application/json
Body:
{ "oldPassword": "secret123", "newPassword": "evenBetter456" }Success:
{ "success": true, "message": "Password changed" }GET /api/user/get-profile/1
Authorization: Bearer <accessToken>
Response:
{
"success": true,
"user": {
"id": 1,
"username": "catman",
"email": "cat@meow.com",
"subscription_type": "free",
"avatar_url": null
}
}PATCH /api/user/update-profile/1
Authorization: Bearer <accessToken>
Content-Type: application/json
Body (any allowed subset):
{ "full_name": "Cat Man", "username": "catman2" }Response:
{ "success": true, "user": { "id": 1, "username": "catman2", "full_name": "Cat Man" } }PATCH /api/user/subscription/1
Authorization: Bearer <accessToken>
Content-Type: application/json
Body:
{ "subscription_type": "plus" }Success:
{ "success": true, "subscription_type": "plus" }POST /api/user/avatar/1
Authorization: Bearer <accessToken>
Content-Type: multipart/form-data
Field: avatar=<image file>
Response:
{
"success": true,
"message": "Avatar updated",
"avatar_url": "https://res.cloudinary.com/.../user_1_..."
}Errors:
{ "success": false, "error": "File too large (max 2MB)" }See .env.example for full list. Main ones:
DATABASE_URL=
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
ACCESS_TOKEN_TTL=30m
REFRESH_TOKEN_DAYS=30
REQUIRE_EMAIL_VERIFICATION=true
MAILTRAP_TOKEN=
CLOUDINARY_*=
ENABLE_WEBSOCKETS=true
ENABLE_SWAGGER=true
socket.join('user:' + userId)
io.to('user:' + userId).emit('something', payload)
It is important for extending the notifications features. ββ
Swagger UI:
http://localhost:3000/api/docs
Raw JSON:
http://localhost:3000/api/docs.json
| Issue | Fix |
|---|---|
| 401 on protected route | Ensure Authorization: Bearer <accessToken> header |
| Avatar upload fails | Check mimetype + size < MAX_AVATAR_SIZE_BYTES |
| Google login fails | Wrong GOOGLE_CLIENT_ID or invalid idToken |
| Verification email not sending | Missing MAILTRAP_TOKEN or disabled flag |
| Socket not connecting | Check ENABLE_WEBSOCKETS=true |
- Fork / branch
- Add feature
- Run lints/tests (when added)
- PR with short description
It is moderately basic implementation. There are a lot of ways to make it more robust, more secure and more user friendly. But this is the limit of me until now ππ, hopefully, I will update it as I learn new things.