Solution
Let's start by setting up a clear folder structure for our project:
/online-bookstore
|-- .env
|-- app.js
|-- /models
| |-- User.js
| |-- Book.js
|-- /routes
| |-- auth.js
| |-- books.js
|-- /middleware
| |-- authorization.js
| |-- logging.js
|-- /node_modules
|-- package.json
Step 1: Project Setup
In your terminal:
mkdir online-bookstore
cd online-bookstore
npm init -y
npm install express mongoose jsonwebtoken bcryptjs dotenv morgan validator helmet express-rate-limit winston
Create a .env file in the root of your project with:
MONGODB_URI="your_mongodb_uri"
JWT_SECRET="your_secret_key"
Step 2: Establish MongoDB Connection
In app.js:
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
dotenv.config();
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const app = express();
Step 3: Basic Express Setup
Continue in app.js:
app.use(express.json());
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Step 4: Model Definitions
In /models/User.js:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: String,
password: String,
role: String,
});
userSchema.pre('save', function(next) {
this.password = bcrypt.hashSync(this.password, 12);
next();
});
module.exports = mongoose.model('User', userSchema);
Step 5: Authentication
In /routes/auth.js:
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const router = express.Router();
router.post('/login', async (req, res) => {
const user = await User.findOne({ username: req.body.username });
if (!user) return res.status(400).send('Invalid username or password.');
const validPassword = bcrypt.compareSync(req.body.password, user.password);
if (!validPassword) return res.status(400).send('Invalid username or password.');
const token = jwt.sign({ _id: user._id, role: user.role }, process.env.JWT_SECRET);
res.send(token);
});
module.exports = router;
Step 6: Authorization Middleware
In /middleware/authorization.js:
const jwt = require('jsonwebtoken');
function authorize(role) {
return (req, res, next) => {
const token = req.header('x-auth-token');
if (!token) return res.status(401).send('No token provided.');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (!decoded || (role && decoded.role !== role)) return res.status(403).send('Forbidden.');
req.user = decoded;
next();
}
}
module.exports = authorize;
Step 7: CRUD for Books
In /models/Book.js:
const mongoose = require('mongoose');
const bookSchema = new mongoose.Schema({
title: String,
author: String,
price: Number,
});
module.exports = mongoose.model('Book', bookSchema);
In /routes/books.js:
const express = require('express');
const Book = require('../models/Book');
const authorize = require('../middleware/authorization');
const router = express.Router();
// All your CRUD operations here, e.g.,
router.get('/', async (req, res) => {
const books = await Book.find();
res.send(books);
});
// Use the authorize middleware as needed, e.g.,
router.post('/', authorize('admin'), async (req, res) => {
const book = new Book(req.body);
await book.save();
res.send(book);
});
module.exports = router;
Step 8: Logging Middleware
In app.js:
const morgan = require('morgan');
app.use(morgan('tiny'));
For error-handling, logging, input validation, security, rate limiting, user registration, and advanced logging, follow the same method to break down your code into modular sections and use middleware appropriately.
Remember to use the app.use('/desired-route', require('./routes/route-file')) to link your route files in the main app.js file.
Let's dive deeper and continue elaborating on the code.
Alternate Step 8: Logging Middleware
You've already added the morgan middleware for basic logging.
In app.js:
const morgan = require('morgan');
app.use(morgan('combined')); // 'combined' gives detailed logs
Step 9: Error-handling
Add a centralized error-handling middleware at the bottom of your app.js:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something went wrong!');
});
Step 10: Input Validation
In /routes/books.js, add some basic validation:
const { check, validationResult } = require('express-validator');
router.post('/',
[
check('title').isLength({ min: 1 }).withMessage('Title is required'),
check('author').isLength({ min: 1 }).withMessage('Author is required'),
check('price').isNumeric().withMessage('Valid price is required'),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// ... rest of the CRUD logic ...
}
);
Part-2
Step 11: Securing the Application
You've already added helmet for basic security.
In app.js:
const helmet = require('helmet');
app.use(helmet());
Step 12: Rate Limiting
Again, in your main app.js:
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
Step 13: User Registration and Validation
In /routes/auth.js, append:
router.post('/register',
[
check('username', 'Username is required').not().isEmpty(),
check('password', 'Password should be at least 6 chars long').isLength({ min: 6 }),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const userExists = await User.findOne({ username: req.body.username });
if (userExists) return res.status(400).send('Username already exists.');
const user = new User(req.body);
await user.save();
res.send('User registered successfully!');
}
);
Step 14: Testing with Postman
At this point, you would open Postman and test the following endpoints:
- POST to
/register with { "username": "your_username", "password": "your_password" }
- POST to
/login with { "username": "your_username", "password": "your_password" } and get a JWT token.
- Test CRUD operations on
/books using the received JWT token.
Step 15: Logging and Monitoring
For advanced logging, use winston.
In app.js:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'online-bookstore-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// To log directly to the console in a development setting:
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
// Replace console.error with logger.error and likewise for other log levels
Now, when you want to log anything, you can use logger.info('Your message'), logger.error('Your error message'), etc. These logs will be saved to the specified files.
This provides a comprehensive backend setup. Remember, in a real-world application, you would also want to set up proper HTTPS, ensure your environment variables are secure, and perhaps consider using a service like MongoDB Atlas for a more secure and scalable MongoDB solution.
Originally posted by @akash-coded in #192 (comment)
Solution
Let's start by setting up a clear folder structure for our project:
Step 1: Project Setup
In your terminal:
mkdir online-bookstore cd online-bookstore npm init -y npm install express mongoose jsonwebtoken bcryptjs dotenv morgan validator helmet express-rate-limit winstonCreate a
.envfile in the root of your project with:Step 2: Establish MongoDB Connection
In
app.js:Step 3: Basic Express Setup
Continue in
app.js:Step 4: Model Definitions
In
/models/User.js:Step 5: Authentication
In
/routes/auth.js:Step 6: Authorization Middleware
In
/middleware/authorization.js:Step 7: CRUD for Books
In
/models/Book.js:In
/routes/books.js:Step 8: Logging Middleware
In
app.js:For error-handling, logging, input validation, security, rate limiting, user registration, and advanced logging, follow the same method to break down your code into modular sections and use middleware appropriately.
Remember to use the
app.use('/desired-route', require('./routes/route-file'))to link your route files in the mainapp.jsfile.Let's dive deeper and continue elaborating on the code.
Alternate Step 8: Logging Middleware
You've already added the
morganmiddleware for basic logging.In
app.js:Step 9: Error-handling
Add a centralized error-handling middleware at the bottom of your
app.js:Step 10: Input Validation
In
/routes/books.js, add some basic validation:Part-2
Step 11: Securing the Application
You've already added
helmetfor basic security.In
app.js:Step 12: Rate Limiting
Again, in your main
app.js:Step 13: User Registration and Validation
In
/routes/auth.js, append:Step 14: Testing with Postman
At this point, you would open Postman and test the following endpoints:
/registerwith{ "username": "your_username", "password": "your_password" }/loginwith{ "username": "your_username", "password": "your_password" }and get a JWT token./booksusing the received JWT token.Step 15: Logging and Monitoring
For advanced logging, use
winston.In
app.js:Now, when you want to log anything, you can use
logger.info('Your message'),logger.error('Your error message'), etc. These logs will be saved to the specified files.This provides a comprehensive backend setup. Remember, in a real-world application, you would also want to set up proper HTTPS, ensure your environment variables are secure, and perhaps consider using a service like MongoDB Atlas for a more secure and scalable MongoDB solution.
Originally posted by @akash-coded in #192 (comment)