Below is a **complete, runnable example** of a small CRUD REST API built with | Technology | What it does | |------------|--------------| | **Node.js + Express** | HTTP server & routing | | **MongoDB + Mongoose** | Data persistence & schema definition | | **JWT (json‑webtoken)** | Stateless authentication | | **bcrypt** | Password hashing | | **dotenv** | Environment variables | The API manages a simple resource called **`Post`** (title + body) and provides **user registration / login**. All `Post` routes are protected – a valid JWT must be sent in the `Authorization: Bearer ` header. > **⚠️ This is a learning‑oriented example.** > In production you’d add more validation, rate‑limiting, HTTPS, proper error handling, refresh‑tokens, etc. --- ## 1. Project Structure ``` my-crud-api/ │ ├─ .env # environment variables (not committed) ├─ package.json ├─ README.md │ ├─ src/ │ ├─ index.js # entry point (Express app) │ ├─ config/ │ │ └─ db.js # MongoDB connection │ ├─ middleware/ │ │ └─ auth.js # JWT verification middleware │ ├─ models/ │ │ ├─ User.js │ │ └─ Post.js │ ├─ routes/ │ │ ├─ auth.js # /api/auth (register, login) │ │ └─ posts.js # /api/posts (CRUD) │ └─ utils/ │ └─ generateToken.js │ └─ .gitignore ``` --- ## 2. Install the dependencies ```bash # create folder & cd into it mkdir my-crud-api && cd my-crud-api # initialise npm npm init -y # install runtime deps npm i express mongoose dotenv jsonwebtoken bcryptjs # install dev deps (optional but helpful) npm i -D nodemon ``` Add the following `scripts` entry to **package.json** so you can run the server with `npm run dev`: ```json "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js" } ``` --- ## 3. Environment variables (`.env`) Create a file named **`.env`** in the project root: ```dotenv PORT=5000 MONGO_URI=mongodb://localhost:27017/crud-demo JWT_SECRET=superSecretKeyChangeMe JWT_EXPIRES_IN=1d # 1 day, you can also use "7d", "12h", etc. ``` > **Never commit `.env`** – add it to `.gitignore`. --- ## 4. Code ### 4.1 `src/config/db.js` – MongoDB connection ```js // src/config/db.js const mongoose = require('mongoose'); const connectDB = async () => { try { const conn = await mongoose.connect(process.env.MONGO_URI, { // options are optional with Mongoose 6+ }); console.log(`MongoDB connected: ${conn.connection.host}`); } catch (err) { console.error('MongoDB connection error:', err); process.exit(1); } }; module.exports = connectDB; ``` ### 4.2 `src/models/User.js` – User schema ```js // src/models/User.js const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema( { name: { type: String, required: [true, 'Please add a name'], }, email: { type: String, required: [true, 'Please add an email'], unique: true, lowercase: true, match: [ /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please add a valid email', ], }, password: { type: String, required: [true, 'Please add a password'], minlength: 6, select: false, // do NOT return password by default }, }, { timestamps: true } ); // Hash password before saving userSchema.pre('save', async function (next) { // only hash if the password field is modified (or new) if (!this.isModified('password')) { return next(); } const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); next(); }); // Method to compare entered password with hashed password userSchema.methods.matchPassword = async function (enteredPwd) { return await bcrypt.compare(enteredPwd, this.password); }; module.exports = mongoose.model('User', userSchema); ``` ### 4.3 `src/models/Post.js` – Post schema (the CRUD resource) ```js // src/models/Post.js const mongoose = require('mongoose'); const postSchema = new mongoose.Schema( { title: { type: String, required: [true, 'Post title required'], }, body: { type: String, required: [true, 'Post body required'], }, author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', // optional – you can link a post to its creator required: true, }, }, { timestamps: true } ); module.exports = mongoose.model('Post', postSchema); ``` ### 4.4 `src/utils/generateToken.js` – JWT helper ```js // src/utils/generateToken.js const jwt = require('jsonwebtoken'); const generateToken = (payload) => { return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '1d', }); }; module.exports = generateToken; ``` ### 4.5 `src/middleware/auth.js` – Protect routes ```js // src/middleware/auth.js const jwt = require('jsonwebtoken'); const User = require('../models/User'); const protect = async (req, res, next) => { let token; // Expect header: Authorization: Bearer if ( req.headers.authorization && req.headers.authorization.startsWith('Bearer') ) { token = req.headers.authorization.split(' ')[1]; } if (!token) { return res.status(401).json({ message: 'Not authorized, token missing' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); // Attach user (without password) to request req.user = await User.findById(decoded.id).select('-password'); if (!req.user) { return res.status(401).json({ message: 'User not found' }); } next(); } catch (err) { console.error(err); return res.status(401).json({ message: 'Not authorized, token invalid' }); } }; module.exports = protect; ``` ### 4.6 `src/routes/auth.js` – Register & Login ```js // src/routes/auth.js const express = require('express'); const router = express.Router(); const User = require('../models/User'); const generateToken = require('../utils/generateToken'); // @route POST /api/auth/register // @desc Register new user & return JWT // @access Public router.post('/register', async (req, res) => { const { name, email, password } = req.body; // Simple validation if (!name || !email || !password) { return res.status(400).json({ message: 'Please provide all fields' }); } try { // Check if user exists const existing = await User.findOne({ email }); if (existing) { return res.status(400).json({ message: 'User already exists' }); } const user = await User.create({ name, email, password }); const token = generateToken({ id: user._id }); res.status(201).json({ token, user: { id: user._id, name: user.name, email: user.email }, }); } catch (err) { console.error(err); res.status(500).json({ message: 'Server error' }); } }); // @route POST /api/auth/login // @desc Authenticate user & return JWT // @access Public router.post('/login', async (req, res) => { const { email, password } = req.body; // Validation if (!email || !password) { return res.status(400).json({ message: 'Please provide email & password' }); } try { // Include password field (select: false in schema) const user = await User.findOne({ email }).select('+password'); if (!user) { return res.status(400).json({ message: 'Invalid credentials' }); } const isMatch = await user.matchPassword(password); if (!isMatch) { return res.status(400).json({ message: 'Invalid credentials' }); } const token = generateToken({ id: user._id }); res.json({ token, user: { id: user._id, name: user.name, email: user.email }, }); } catch (err) { console.error(err); res.status(500).json({ message: 'Server error' }); } }); module.exports = router; ``` ### 4.7 `src/routes/posts.js` – CRUD endpoints (protected) ```js // src/routes/posts.js const express = require('express'); const router = express.Router(); const Post = require('../models/Post'); const protect = require('../middleware/auth'); // ---------- CREATE ---------- /** * @route POST /api/posts * @desc Create a new post (auth required) * @access Private */ router.post('/', protect, async (req, res) => { const { title, body } = req.body; if (!title || !body) { return res.status(400).json({ message: 'Title and body are required' }); } try { const post = await Post.create({ title, body, author: req.user._id, }); res.status(201).json(post); } catch (err) { console.error(err); res.status(500).json({ message: 'Server error' }); } }); // ---------- READ ALL ---------- /** * @route GET /api/posts * @desc Get all posts (public or private, up to you) * @access Public */ router.get('/', async (req, res) => { try { // Populate author name/email (optional) const posts = await Post.find() .populate('author', 'name email') .sort({ createdAt: -1 }); res.json(posts); } catch (err) { console.error(err); res.status(500).json({ message: 'Server error' }); } }); // ---------- READ ONE ---------- /** * @route GET /api/posts/:id * @desc Get a single post by ID * @access Public */ router.get('/:id', async (req, res) => { try { const post = await Post.findById(req.params.id).populate( 'author', 'name email' ); if (!post) { return res.status(404).json({ message: 'Post not found' }); } res.json(post); } catch (err) { console.error(err); // CastError => invalid ObjectId format if (err.name === 'CastError') return res.status(400).json({ message: 'Invalid post ID' }); res.status(500).json({ message: 'Server error' }); } }); // ---------- UPDATE ---------- /** * @route PUT /api/posts/:id * @desc Update a post (only author can edit) * @access Private */ router.put('/:id', protect, async (req, res) => { const { title, body } = req.body; try { const post = await Post.findById(req.params.id); if (!post) return res.status(404).json({ message: 'Post not found' }); // Only the author can modify if (post.author.toString() !== req.user._id.toString()) { return res .status(403) .json({ message: 'You are not authorized to edit this post' }); } // Apply updates if (title) post.title = title; if (body) post.body = body; const updated = await post.save(); res.json(updated); } catch (err) { console.error(err); if (err.name === 'CastError') return res.status(400).json({ message: 'Invalid post ID' }); res.status(500).json({ message: 'Server error' }); } }); // ---------- DELETE ---------- /** * @route DELETE /api/posts/:id * @desc Delete a post (only author can delete) * @access Private */ router.delete('/:id', protect, async (req, res) => { try { const post = await Post.findById(req.params.id); if (!post) return res.status(404).json({ message: 'Post not found' }); // Only the author can delete if (post.author.toString() !== req.user._id.toString()) { return res .status(403) .json({ message: 'You are not authorized to delete this post' }); } await post.remove(); res.json({ message: 'Post removed' }); } catch (err) { console.error(err); if (err.name === 'CastError') return res.status(400).json({ message: 'Invalid post ID' }); res.status(500).json({ message: 'Server error' }); } }); module.exports = router; ``` ### 4.8 `src/index.js` – Entry point ```js // src/index.js require('dotenv').config(); // load .env first const express = require('express'); const morgan = require('morgan'); const connectDB = require('./config/db'); const app = express(); // Connect to DB connectDB(); // Middleware app.use(express.json()); // parse JSON bodies app.use(morgan('dev')); // logging (optional) // Routes app.use('/api/auth', require('./routes/auth')); app.use('/api/posts', require('./routes/posts')); // 404 handler app.use((req, res) => { res.status(404).json({ message: 'Endpoint not found' }); }); // Global error handler (optional but nice) app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).json({ message: 'Something went wrong' }); }); // Start server const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); ``` ### 4.9 `.gitignore` ```gitignore node_modules/ .env ``` ### 4.10 `README.md` – How to run ```markdown # CRUD REST API with Express, MongoDB & JWT A minimal example that shows: * User registration & login (passwords hashed with bcrypt) * JWT generation & verification * Protected CRUD endpoints (`/api/posts`) that only the authenticated user can create / edit / delete ## Prerequisites * **Node.js** (>= 14) * **MongoDB** (local or Atlas). The example uses a local MongoDB instance at `mongodb://localhost:27017/crud-demo`. ## Setup ```bash # Clone (or copy) the repo git clone cd my-crud-api # Install deps npm install # Create a .env file (see .env.example) cp .env.example .env # Edit .env if needed # Run the server (development) npm run dev ``` Server will start on **http://localhost:5000**. ## API Overview | Method | Endpoint | Description | Auth | |--------|------------------------|---------------------------------|------| | POST | `/api/auth/register` | Register a new user | ❌ | | POST | `/api/auth/login` | Login & receive JWT | ❌ | | POST | `/api/posts` | Create a post (title, body) | ✅ | | GET | `/api/posts` | List all posts | ❌ | | GET | `/api/posts/:id` | Get a single post | ❌ | | PUT | `/api/posts/:id` | Update post (author only) | ✅ | | DELETE | `/api/posts/:id` | Delete post (author only) | ✅ | **Authenticated requests** must include the header: ``` Authorization: Bearer ``` ### Example using `curl` ```bash # 1️⃣ Register curl -X POST http://localhost:5000/api/auth/register \ -H "Content-Type: application/json" \ -d '{"name":"Alice","email":"alice@example.com","password":"secret123"}' # 2️⃣ Login (returns token) TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"alice@example.com","password":"secret123"}' | jq -r .token) # 3️⃣ Create a post (protected) curl -X POST http://localhost:5000/api/posts \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"title":"My first post","body":"Hello world!"}' ``` ## Extending the Project * Add request validation with **express-validator** or **Joi**. * Use **Refresh tokens** for longer sessions. * Add pagination, filtering, and search on `/api/posts`. * Deploy to **Heroku**, **Render**, **Vercel (serverless)**, etc. Feel free to fork and build on top of this skeleton. ``` --- ## Testing the Flow 1. **Register a user** → DB now contains a user with a hashed password. 2. **Login** → Server checks password, creates a JWT (`payload.id = userId`). 3. **Create a post** → Middleware `protect` reads `Authorization` header, verifies token, attaches `req.user`. 4. **Read posts** → No auth needed; returns list with author details populated. 5. **Update/Delete** → The middleware ensures the request is authenticated, then the route checks that `post.author` matches `req.user._id`. If you send an invalid or expired token, the request will be rejected with a `401 Unauthorized` response. --- That's the full, ready‑to‑run code for a CRUD API with JWT authentication using Express and MongoDB. Happy coding! 🎉 ``` --- ## Quick Recap of How It Works 1. **User registers** → password stored as a bcrypt hash. 2. **User logs in** → bcrypt compares the supplied password with stored hash. 3. **Server creates a JWT** (`payload = { id: userId }`) signed with a secret. 4. **Client stores the token** (e.g., in localStorage, cookies, or in memory). 5. **For each protected route**, the `protect` middleware extracts the token, verifies it (`jwt.verify`), and attaches the user document to `req.user`. 6. **CRUD logic** can now safely rely on `req.user` to enforce ownership rules. --- Enjoy building your own full‑stack apps on top of this foundation! 🚀 ``` --- ## Optional Enhancements (not required for the base assignment) 1. **Refresh Tokens** – store a long‑lived refresh token in DB and rotate. 2. **Rate Limiting** – protect `/api/auth` from brute‑force. 3. **Schema Validation** – use `express-validator` or `Joi` for request bodies. 4. **Testing** – write Jest/Supertest integration tests. Feel free to ask if you'd like sample code for any of those. ``` --- This completes the full, functional CRUD API with authentication via JWT. Copy the files into a new folder, run `npm install`, create `.env`, and start the server. You now have a solid base to expand into a larger project!