PermalinkIntroduction
A blog API allows client applications to create, view, edit, delete and even filter blog content. In this article, we are going to create a simple blog API using Node.js, Express.js, and MongoDB
PermalinkPrerequisite
A basic understanding of the following:
Node.js
Javascript
Express.js
make sure you install the following on your computer:
Node.js
Code Editor (eg VS code)
PermalinkAPI Structure
Our API will have the following endpoints
Unprotected Routes
Method | Route | Function |
GET | / | Return Homepage |
GET | /articles | Return all published articles |
GET | /articles/:articleId | Return a single article by ID |
POST | /sign-up | Return Homepage |
POST | /login | Return all published articles |
Protected Routes
Method | Route | Function |
GET | /user/articles | Return all articles created by the user |
POST | /create-article | Create a new article |
PUT | /edit-article/:articleId/update-state | Update the state of an article |
PUT | /edit-article/:articleId | Edit article |
DELETE | /delete-article/:articleId | Deletes an article |
PermalinkImplementation
PermalinkProject Setup
Create a new folder and open it in your code editor
Go to your terminal, navigate to the folder you created and execute the following command
npm init -y
Install express package
npm i express
Create an ‘index.js’ file, inside this file require an express Module, and create a constant ‘app’ for creating an instance of the express module. then export app
const express = require("express"); const app = express(); module.exports = app;
Install mongoose package
npm i mongoose
Sign in to https://www.mongodb.com/ or create an account if you don't have one.
Create a new Project, Cluster, User, and add your IP to the IP access list. If you are not sure how to do that visit https://www.mongodb.com/docs/manual/tutorial/getting-started/
Click on connect to get your connection string, it should look like this
mongodb+srv://<user>:<password>@cluster0.00o4wev.mongodb.net/blog?
create a '.env' file to store our environment variable, inside this file add your database connection string and name it MONGODB_URI
MONGODB_URI=mongodb+srv://<user>:<password>@cluster0.00o4wev.mongodb.net/blog?
Create a folder, name it 'utils' and create a 'database.js' file. add the following code inside database.js
const mongoose = require('mongoose'); const connect = (url) => { mongoose.connect(url || 'mongodb://localhost:27017') mongoose.connection.on("connected", () => { console.log("Connected to MongoDB Successfully"); }); mongoose.connection.on("error", (err) => { console.log("An error occurred while connecting to MongoDB"); console.log(err); }); } module.exports = { connect };
Create an 'app.js' file, inside the file require index.js, database.js, and dotenv.
Read MONGODB_URI from the .env fileThen call connect() function from database.js and app.listen() from the index.js passing the MONGODB_URI and PORT number respectively
const app = require('./index'); require("dotenv").config(); const Database = require("./utils/database"); const PORT = 8080 const MONGODB_URI = process.env.MONGODB_URI; Database.connect(MONGODB_URI); app.listen(PORT, () => { console.log(`server running on http://localhost:${PORT}`); });
PermalinkAuthentication
Routes
In this section, we will implement user sign-up and log-in, we will make use of passport - an authentication middleware for Node.js
install the following packages
npm I passport passport-jwt passport-local jsonwebtoken
Create a 'routes' folder and create an 'auth.js' file inside the folder.
Inside auth.js, import express, passport, jsonwebtoken and dotenv
const express = require("express"); const passport = require("passport"); const jwt = require('jsonwebtoken'); require('dotenv').config();
Create a const ' router'
const router = express.Router();
Implement the sign-up route as shown below
router.post( "/sign-up", passport.authenticate("signup", { session: false }), async (req, res, next) => { res.status(201).json({ message: "Signup successful", user: req.user, }); } );
add JWT_SECRET - 'a random string' to the '.env' we created earlier, It will be used in validating jwt tokens
JWT_SECRET=djnisdnrosdnfodjnfonsdfnsd
Implement the login route as shown below
router.post("/login", async (req, res, next) => { passport.authenticate("login", async (err, user, info) => { try { if (err) { return next(err); } if (!user) { const error = new Error("Username or password is incorrect here"); return next(error); } req.login(user, { session: false }, async (error) => { if (error) return next(error); const body = { _id: user._id, email: user.email }; const token = jwt.sign({ user: body }, process.env.JWT_SECRET, { expiresIn: '1h' }); return res.json({ token }); }); } catch (error) { return next(error); } })(req, res, next); });
export the router
module.exports = router;
Import it into index.js and add the route
const authRoute = require("./routes/auth"); app.use("/", authRoute);
Model
Create a 'models' folder and a 'user.js' file
import mongoose
const mongoose = require("mongoose");
Create a const 'Schema'
const Schema = mongoose.Schema;
Create the userSchema
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
first_name: {
type: String,
required: true,
},
last_name: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
});
Install bycrypt, It will be used to hash and validate password `npm I bycrypt'
Add pre-hook - Processing scripts that hashes the password before saving it to the database
userSchema.pre(
'save',
async function (next) {
const user = this;
const hash = await bcrypt.hash(this.password, 12);
this.password = hash;
next();
}
);
- Add isValidPassword method, we will use it to validate password
userSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
- Export the model
module.exports = mongoose.model("User", userSchema);
Controller
Create a 'controllers' folder and create an 'auth.js' file
Inside auth.js, import passport, passport-local, passport-jwt and our user model
const passport = require("passport"); const localStrategy = require("passport-local").Strategy; const JWTstrategy = require("passport-jwt").Strategy; const ExtractJWT = require("passport-jwt").ExtractJwt; const User = require("../models/user");
Implement Jwt Streategy
passport.use( new JWTstrategy( { secretOrKey: process.env.JWT_SECRET, jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), }, async (token, done) => { try { return done(null, token.user); } catch (error) { done(error); } } ) );
Implement the sign-up logic
passport.use( "signup", new localStrategy( { usernameField: "email", passwordField: "password", passReqToCallback: true, }, async (req, email, password, done) => { try { const first_name = req.body.first_name; const last_name = req.body.last_name; const user = await User.create({ email, password, first_name, last_name, }); return done(null, user); } catch (error) { done(error); } } ) );
Implement the login logic
passport.use( "login", new localStrategy( { usernameField: "email", passwordField: "password", }, async (email, password, done) => { try { const user = await User.findOne({ email }); if (!user) { return done(null, false, { message: "User not found" }); } const validate = await user.isValidPassword(password); if (!validate) { return done(null, false, { message: "Wrong Password" }); } return done(null, user, { message: "Logged in Successfully" }); } catch (error) { console.log(error); return done(error); } } ) );
Now we are done with the authentication section, a user can sign up and log in, moving on we will work on implementing the function a logged-in user can perform
PermalinkUser
Model
create a article.js file in the model folder
Import mongoose
const mongoose = require("mongoose");
create a constant, Schema
const Schema = mongoose.Schema;
create articleSchema
const articleSchema = new Schema({ title: { type: String, required: true, unique: true }, description: String, author: { type: Schema.Types.ObjectId, ref: 'User', required: true }, state: { type: String, enum: ["draft", "published"], default: "draft", required: true }, read_count: { type: Number, default: 0, required: true }, reading_time: String, tags: [String], body: { type: String, required: true, }, timestamp: { type: Date, default: Date.now, required: true, }, });
export the model
module.exports = mongoose.model("Article", articleSchema);
Controllers
Create a 'user.js' file in the controllers' folder
Import the articles model
Implement getArticles logic
exports.getArticles = async (req, res, next) => { const { query } = req; const { state, page = 0, per_page = 5 } = query; const findQuery = { author: req.user }; if (state) { findQuery.state = state; } try { const articles = await Article .find(findQuery) .skip(page) .limit(per_page) res.json(articles); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Implement postCreateArticle logic
exports.postCreateArticle = async (req, res, next) => { const title = req.body.title; const description = req.body.description; const tags = req.body.tags.split(",").map((tag) => { return tag.trim(); }); const body = req.body.body; const author = req.user; const reading_time = calcReadingTime.calcReadingTime(body); if (await Article.findOne({ title })) { res.status(202).json({message : "title already exist, try something different"}); return; } const article = new Article({ title, description, author, reading_time, tags, body, }); try { await article.save(); res.status(201).json(article); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Implement postEditArticle
exports.postEditArticle = async (req, res, next) => { const updatedTitle = req.body.title; const updatedDescription = req.body.description; const updatedTags = req.body.tags.split(",").map((tag) => { return tag.trim(); }); const updatedBody = req.body.body; const updatedReading_time = calcReadingTime.calcReadingTime(updatedBody); const articleId = req.params.articleId; if (await Article.findOne({ title: updatedTitle })) { console.log(Article.findOne({ title: updatedTitle })); res.status(202).json({message : "title already exist, try something different"}); return; } try { const article = await Article.findOne({ _id: articleId, author: req.user }); article.title = updatedTitle; article.description = updatedDescription; article.reading_time = updatedReading_time; article.tags = updatedTags; article.body = updatedBody; await article.save(); res.status(201).json(article); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Implement updateState
exports.postUpdateState = async (req, res, next) => { const articleId = req.params.articleId; try { const article = await Article.findOne({ _id: articleId, author: req.user }); article.state = "published"; await article.save(); res.status(201).json(article); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Implement delete
exports.postDeletetArticle = async (req, res, next) => { const articleId = req.params.articleId; try { const response = await Article.deleteOne({ _id: articleId, author: req.user, }); res.status(202).json(response); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Routes
1. Create a user.js file in the routes folder
2. Import express and create a constant router
3. Import the controller user.js file into the controller folder
javascript const express = require('express') const router = express.Router()
const userController = require('../controllers/user'
4. Add the following routes and pass in the right controller
javascript router.get('/articles', userController.getArticles)
router.post('/create-article', userController.postCreateArticle)
router.put('/edit-article/:articleId/update_state', userController.postUpdateState)
router.put('/edit-article/:articleId', userController.postEditArticle)
router.delete('/delete-article/:articleId', userController.postDeletetArticle)
5. export router module.exports = router
6. Import router into index.js
javascript const userRoute = require("./routes/user");
7. add user route, and pass in passport.authenticate to protect route
javascript app.use("/user", passport.authenticate("jwt",{ session: false, failureRedirect: "/login"}),userRoute);
PermalinkBlog
We will now implement the endpoints that allowed both logged-in and not logged-in users to access published blogs
Controllers
1. create a blog.js file in the controllers folder
2. Import our articles model Article = require("../models/article");
3. Implement getIndex
javascript exports.getIndex = async (req, res, next) => { res.json({message: "welcome to AltBlog"}) }
4. Implement getArticles
javascript exports.getArticles = async (req, res, next) => { const { query } = req; const { author, title, tags,
order = "asc", order_by = "timestamp",
page = 0, per_page = 20, } = query;
const findQuery = { state: "published" };
if (author) { findQuery.author = author; }
if (title) { findQuery.title = title; }
if (tags) { findQuery.tags = { $in: tags.split(",").map((tag) => { return tag.trim(); }), }; }
const sortQuery = {};
const sortAttributes = order_by.split(",").map((order) => { return order.trim(); });;
for (const attribute of sortAttributes) { if (order === "asc" && order_by) { sortQuery[attribute] = 1; }
if (order === "desc" && order_by) { sortQuery[attribute] = -1; } }
try { const articles = await Article.find(findQuery) .sort(sortQuery) .skip(page) .limit(per_page);
res.json(articles); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
5. Implement getArticle to return a single article by id
javascript exports.getArticle = async (req, res, next) => { const { articleId } = req.params;
try { const article = await Article.findOne({ _id: articleId, state: "published", }).populate("author", "first_name last_name email");
article.read_count++;
article.save();
res.json(article); } catch (err) { res.status(500).json({message: "an error occured"}); console.log(err); } };
Routes
1. create a blog.js file in the routes folder
2. Import express and create a router constant
javascript const express = require('express') const router = express.Router()
3. Import blog.js from the controller file const blogController = require('../controllers/blog')
4. Implement the following routes and pass in the right controller
javascript router.get('/', blogController.getIndex)
router.get('/articles', blogController.getArticles)
router.get('/articles/:articleId', blogController.getArticle)
5. export router module.exports = router
6. Import bog.js into index.js and add the route
javascript const userRoute = require("./routes/user");
app.use("/", blogRoute);
PermalinkError Controller
The error handles error and return the right message to the client
1. Create an 'error.js' file in the controllers' folder
2. Add the following function to return 'page not found' when the clients tried to access and route that doesn't exist
javascript exports.error404 = (req, res, next) => { res.status(404).json({ message: '404 page not found' }) }
3. Import it into index.js and add it to the routes
javascript const errorController = require("./controllers/error");
app.use("/", errorController.error404);
PermalinkRunning the API
At this point, we are ready to run our API. to do that go to the terminal and execute node app.js
, this will start the app
PermalinkConclusion
And this is how you can create a simple blog API with Node.js.
More features such as validation, views, and tests can be added as well