A beginner-friendly Express + MongoDB authentication API with:
- user registration and login
- refresh token rotation (
/users/refresh) - logout with refresh token invalidation (
/users/logout) - JWT-based protected route (
/users/me) - password hashing with Argon2
- request logging (optional SolarWinds forwarding)
- centralized error handling
This README is written so students can implement almost the same project from scratch.
- Node.js (ES modules)
- Express 5
- MongoDB + Mongoose
- Argon2 (password hashing)
- JSON Web Token (
jsonwebtoken) http-errors+http-status-codes- Axios (for optional SolarWinds log shipping)
pnpm+nodemon
config/
db.js
solarwinds.js
controllers/
auth.controller.js
middleware/
auth.middleware.js
error.middleware.js
logger.middleware.js
models/
user.model.js
routes/
auth.routes.js
utils/
jwt.js
index.js
- Install dependencies.
pnpm install- Create
.envin the project root.
PORT=3000
MONGO_URI=your_mongo_connection_string
JWT_SECRET=your_super_secret_key
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=your_refresh_secret_key
JWT_REFRESH_EXPIRES_IN=7d
SOLARWINDS_TOKEN=optional- Start development server.
pnpm dev- For production mode.
pnpm startStartup flow:
- Load environment variables (
dotenv.config()). - Connect to MongoDB (
connectDB()). - Register middlewares (
cors(),express.json(),loggerMiddleware). - Mount auth routes at
/users. - Add health/base route
/. - Register
errorMiddlewareas the last middleware. - Listen on
PORT(default3000).
Why error middleware is last:
- Express forwards thrown errors to the next error handler.
- If placed earlier, it will not catch errors from later route handlers.
- Reads
MONGO_URIfrom.env. - Throws a clear error if missing.
- Connects with
mongoose.connect(uri).
User schema fields:
fullName: required, trimmed stringemail: required, unique, lowercased, trimmed, regex validatedpassword: required, minimum length 6refreshToken: stored refresh token string (ornullon logout)
Custom validation messages included:
"Full name is required""Email is required""Please provide a valid email address""Password is required""Password must be at least 6 characters long"
Security behavior:
pre("save")hook hashes password using Argon2.- Hashing runs only when password is modified.
comparePassword(candidatePassword)verifies login password.
getJwtSecret()ensuresJWT_SECRETexists.getRefreshSecret()ensuresJWT_REFRESH_SECRETexists.generateToken(userId)signs payload{ userId }.generateToken(userId)usesJWT_EXPIRES_IN(default15m).generateRefreshToken(userId)usesJWT_REFRESH_EXPIRES_IN(default7d).verifyAccessToken(token)verifies access tokens.verifyRefreshToken(token)verifies refresh tokens.
- Expects header format:
Authorization: Bearer <token> - Verifies token with
verifyAccessToken(...) - Stores decoded payload in
req.user - Returns
401 Unauthorizedwith"Unauthorized"when header/token missing. - Returns
401 Unauthorizedwith"Invalid or expired token"when verification fails.
- Tracks request start and finish time.
- Creates a structured log object after response is sent.
- Calls
sendToSolarWinds(logEntry).
- Central error formatter.
- Uses
err.statusCodeorerr.statuswhen available. - Falls back to
500 Internal Server Error. - Returns JSON:
{
"success": false,
"message": "..."
}- Includes stack trace only when
NODE_ENV=development.
- If
SOLARWINDS_TOKENis not set, it silently skips sending logs. - If token exists, it POSTs logs to SolarWinds collector API.
- Errors are printed in console but do not break API responses.
This keeps observability optional for classroom/local setups.
POST /users/register->registerUserPOST /users/login->loginUserPOST /users/refresh->refreshAccessTokenPOST /users/logout->authMiddleware->logoutUserGET /users/me->authMiddleware->getCurrentUser
- Reads
fullName,email,passwordfrom request body. - Creates user with
User.create(...). - Returns
201 Createdand safe user fields (no password).
- Finds user by email.
- Validates password via
comparePassword. - Throws
401("Invalid email or password") on failure. - Returns
access_token,refresh_token,token_type, and user basics on success.
- Requires
refresh_tokenin request body. - Verifies JWT signature and expiry with
verifyRefreshToken. - Confirms the refresh token matches the one stored for that user.
- Rotates tokens on success (issues new access + refresh token pair).
- Returns
401for invalid/expired/mismatched refresh tokens.
- Requires a valid access token.
- Clears stored
refreshTokenfor the authenticated user. - Returns
200with a logout confirmation message.
- Uses
req.user.userIdfrom auth middleware. - Fetches user and excludes password via
.select("-password"). - Throws
404if user no longer exists.
http://localhost:3000
Request:
{
"fullName": "Jane Doe",
"email": "jane@example.com",
"password": "strongPassword123"
}Success (201):
{
"message": "User registered successfully",
"data": {
"id": "...",
"fullName": "Jane Doe",
"email": "jane@example.com",
"createdAt": "..."
}
}Request:
{
"email": "jane@example.com",
"password": "strongPassword123"
}Success (200):
{
"message": "Login successful",
"data": {
"access_token": "<jwt>",
"refresh_token": "<refresh-jwt>",
"token_type": "Bearer",
"id": "...",
"fullName": "Jane Doe",
"email": "jane@example.com"
}
}Request:
{
"refresh_token": "<refresh-jwt>"
}Success (200):
{
"message": "Token refreshed successfully",
"data": {
"access_token": "<new-access-jwt>",
"refresh_token": "<new-refresh-jwt>",
"token_type": "Bearer"
}
}Header:
Authorization: Bearer <jwt>Success (200):
{
"message": "Logged out successfully"
}Header:
Authorization: Bearer <jwt>Success (200):
{
"message": "User fetched successfully",
"data": {
"_id": "...",
"fullName": "Jane Doe",
"email": "jane@example.com",
"createdAt": "...",
"updatedAt": "..."
}
}- Validation failure while registering:
- by default this codebase returns an error via
errorMiddleware(typically500unless status is set), and the message includes Mongoose validation text. - Wrong login credentials:
401with"Invalid email or password".- Missing or invalid token:
401with"Unauthorized"or"Invalid or expired token".- Missing/invalid/expired refresh token:
400("Refresh token is required") or401("Invalid or expired refresh token").- Missing
JWT_SECRETorMONGO_URI: - startup/runtime error with clear message in console.
- Missing
JWT_REFRESH_SECRET: - startup/runtime error with clear message in console.
Register:
curl -X POST http://localhost:3000/users/register \
-H "Content-Type: application/json" \
-d '{"fullName":"Jane Doe","email":"jane@example.com","password":"strongPassword123"}'Login:
curl -X POST http://localhost:3000/users/login \
-H "Content-Type: application/json" \
-d '{"email":"jane@example.com","password":"strongPassword123"}'Refresh access token (replace <REFRESH_TOKEN>):
curl -X POST http://localhost:3000/users/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<REFRESH_TOKEN>"}'Get current user (replace <TOKEN>):
curl http://localhost:3000/users/me \
-H "Authorization: Bearer <TOKEN>"Logout (replace <TOKEN>):
curl -X POST http://localhost:3000/users/logout \
-H "Authorization: Bearer <TOKEN>"- Setup Express app and folder structure.
- Add Mongo connection helper (
config/db.js). - Create user model with validation + Argon2 hooks.
- Add JWT utility functions.
- Implement register, login, refresh, and logout controllers.
- Add auth middleware and protected routes (
/users/me,/users/logout). - Add token rotation logic for
/users/refresh. - Add error middleware and use
http-errorsfor consistency. - Add logger middleware and optional SolarWinds forwarding.
- Test all endpoints with Postman or cURL.
- Use
pnpmfor consistency with this project.