create-express-auth-boilerplate
v1.0.1
Published
Basic Node.js + Express auth boilerplate (ESM) with JWT, MongoDB, and global error handling
Maintainers
Readme
Node.js Auth Boilerplate (ESM)
Basic Express + MongoDB + JWT (access + refresh token) auth boilerplate with a clean layered structure and global error handling.
Folder structure
src/
├── config/ # db.js - MongoDB connection
├── controllers/ # req/res handling only, delegates to services
├── middleware/ # auth guard, 404 handler, global error handler
├── models/ # Mongoose schemas (plain — no hashing logic)
├── routes/ # Express routers
├── services/ # business logic (signup, login, refresh, logout)
├── utils/ # AppError, password.util.js, token.util.js
├── validations/ # Joi schemas + validate() middleware factory
└── app.js # Express app setup (middleware + routes wired here)
server.js # entry point: loads env, connects DB, starts serverSetup
npm install
cp .env.example .env # then fill in your own values
npm run devRequest flow
route -> validate(schema) -> controller -> service -> model
|
v (on error)
next(error) -> errorHandler.jsEvery error, thrown in a service, a Mongoose validation failure, a duplicate
email (E11000), or a bad/expired token, flows through next(error) into
src/middleware/errorHandler.js, the single place responses get shaped.
Password & token handling
src/utils/password.util.js—hashPassword()/comparePassword(). The model is a plain schema; hashing happens in the service layer beforeUser.create(), not in a Mongoose hook.src/utils/token.util.js—generateAccessToken(),generateRefreshToken(),verifyAccessToken(),verifyRefreshToken().- The refresh token is hashed (same bcrypt util as passwords) before being
stored on the user document, and rotated (a new one issued) every time
/refresh-tokenis called — so a leaked old refresh token stops working once it's been used once.
Endpoints
| Method | Route | Auth required | Description | |--------|-------------------------|----------------|---------------------------------------| | POST | /api/auth/signup | No | Register a new user | | POST | /api/auth/login | No | Log in, get access + refresh tokens | | POST | /api/auth/refresh-token | No (body) | Exchange refresh token for a new pair | | POST | /api/auth/logout | Yes (Bearer) | Revoke the stored refresh token | | GET | /api/auth/me | Yes (Bearer) | Get current user |
Signup / Login response shape
{
"success": true,
"message": "Login successful",
"data": {
"user": { "id": "...", "name": "...", "email": "..." },
"accessToken": "short-lived-jwt",
"refreshToken": "long-lived-jwt"
}
}Refresh token
curl -X POST http://localhost:5000/api/auth/refresh-token \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<refresh_token_from_login>"}'Logout (protected — revokes the refresh token)
curl -X POST http://localhost:5000/api/auth/logout \
-H "Authorization: Bearer <access_token>"Get current user (protected)
curl http://localhost:5000/api/auth/me \
-H "Authorization: Bearer <access_token>"Notes
- Access tokens are short-lived (
15mdefault) and sent on every request viaAuthorization: Bearer <token>. Refresh tokens are long-lived (7ddefault) and only ever sent to/refresh-token. - Access and refresh tokens use different secrets (
JWT_ACCESS_SECRET,JWT_REFRESH_SECRET) so a leaked one can't be used to forge the other. AppErrormarks errors asisOperational: trueso the error handler can distinguish expected errors (bad input, wrong password, expired token) from real bugs.- For production, consider sending the refresh token as an
httpOnlycookie instead of in the JSON body — this boilerplate keeps it in the body for simplicity, but the service layer doesn't care which transport you use. - Add new resources by copying the
authslice's pattern: model -> service -> controller -> routes -> mount inapp.js.
