In part one of this series, we set up our development environment and a basic file structure. Now, let's design the API itself. In this article, we'll cover the following topics:
Deciding on the routes and functionality of our API
Defining the data models for our database
Implementing the data models
Setting up the database connection
Let's get started!
Deciding on the Routes and Functionality
The first step in designing our API is to decide on the routes and functionality we want to include.
Let's outline the requirements for our blog:
A user should be able to sign up and sign in to the blog app
A blog can be in two states: draft and published
A user should be able to get a list of published articles, whether logged in or not
A user should be able to get a published article, whether logged in or not
A logged in user should be able to create an article.
When an article is created, it should be in draft state.
The author of the article should be able to update the state of the article to published
The author of the article should be able to edit the article in draft or published state
The author of the article should be able to delete the article in draft or published state
The author of the article should be able to get a list of their articles. The endpoint should be paginated and filterable by state
Articles created should have title, cover image, description, tags, author, timestamp, state, read count, reading time and body.
-
The list of articles endpoint that can be accessed by both logged in and not logged in users should be paginated.
- It should be searchable by author, title and tags.
- It should also be orderable by read count, reading time and timestamp
When a single article is requested, the API should return the author's information with the blog, and the read count of the blog should increase by 1.
Considering the outlined requirements, we'll define our routes as follows:
-
Blog routes:
- GET /blog: Retrieve a list of all published articles
- GET /blog/:article_id: Retrieve a single article by its ID
-
Author routes: We want only authenticated users to have access to these routes and all of the CRUD operations.
- GET /author/blog: Retrieve a list of all published articles created by the user.
- POST /author/blog: Create a new article
- PATCH /author/blog/edit/:article_id: Update an article by its ID
- PATCH /author/blog/edit/state/:article_id: Update an article's state
- DELETE /author/blog/:article_id: Delete an article by its ID
-
Auth routes: for managing user authentication.
- POST /auth/signup: Register a new user
- POST /auth/login: Log in an existing user
Defining the Data Models
With the routes defined, we can start thinking about the data models for our database. A data model is a representation of the data that will be stored in the database and the relationships between that data. We'll be using Mongoose to define our schema.
We will have two data models: Blog and User.
User
| field | data_type | constraints |
|---|---|---|
| firstname | String | required |
| lastname | String | required |
| String | required, unique, index | |
| password | String | required |
| articles | Array, [ObjectId] | ref - Blog |
Blog
| field | data_type | constraints |
|---|---|---|
| title | String | required, unique, index |
| description | String | |
| tags | Array, [String] | |
| imageUrl | String | |
| author | ObjectId | ref - Users |
| timestamp | Date | |
| state | String | required, enum: ['draft', 'published'], default:'draft' |
| readCount | Number | default:0 |
| readingTime | String | |
| body | String | required |
Mongoose has a method called populate(), which lets you reference documents in other collections. populate() will automatically replace the specified paths in the document with document(s) from other collection(s). The User model has its articles field set to an array of ObjectId's. The ref option is what tells Mongoose which model to use during population, in this case the Blog model. All _id's we store here must be article _id's from the Blog model. Similarly, the Blog model references the User model in its author field.
Implementing the data models
- In
/src/models, create a file calledblog.model.jsand set up the Blog model:
const mongoose = require("mongoose");
const uniqueValidator = require('mongoose-unique-validator');
const { Schema } = mongoose;
const BlogSchema = new Schema({
title: { type: String, required: true, unique: true, index: true },
description: String,
tags: [String],
author: { type: Schema.Types.ObjectId, ref: "Users" },
timestamp: Date,
imageUrl: String,
state: { type: String, enum: ["draft", "published"], default: "draft" },
readCount: { type: Number, default: 0 },
readingTime: String,
body: { type: String, required: true },
});
// Apply the uniqueValidator plugin to the blog model
BlogSchema.plugin(uniqueValidator);
const Blog = mongoose.model("Blog", BlogSchema);
module.exports = Blog;
The title field is defined as a required string and must be unique across all documents in the collection. The description field is defined as a string, and the tags field is defined as an array of strings. The author field is defined as a reference to a document in the users collection, and the timestamp field is defined as a date. The imageUrl field is defined as a string, the state field is defined as a string with a set of allowed values (either "draft" or "published"), and the readCount field is defined as a number with a default value of 0. The readingTime field is defined as a string, and the body field is defined as a required string.
mongoose-unique-validator is a plugin that adds pre-save validation for unique fields within a Mongoose schema. It will validate the unique option in the schema and prevent the insertion of a document if the value of a unique field already exists in the collection.
- In
/src/models, create a file calleduser.model.jsand set up the User model:
const mongoose = require("mongoose");
const uniqueValidator = require("mongoose-unique-validator");
const bcrypt = require("bcrypt");
const { Schema } = mongoose;
const UserModel = new Schema({
firstname: { type: String, required: true },
lastname: { type: String, required: true },
email: {
type: String,
required: true,
unique: true,
index: true,
},
password: { type: String, required: true },
articles: [{ type: Schema.Types.ObjectId, ref: "Blog" }],
});
// Apply the uniqueValidator plugin to the user model
UserModel.plugin(uniqueValidator);
UserModel.pre("save", async function (next) {
const user = this;
if (user.isModified("password") || user.isNew) {
const hash = await bcrypt.hash(this.password, 10);
this.password = hash;
} else {
return next();
}
});
const User = mongoose.model("Users", UserModel);
module.exports = User;
The firstname and lastname fields are defined as required strings, and the email field is defined as a required string and must be unique across all documents in the collection. The password field is defined as a required string, and the articles field is defined as an array of references to documents in the Blog collection.
The pre hook is used to add a function that will be executed before a specific Mongoose method is run. The pre-save hook here hashes the user's password with the npm module bcrypt, before the user document is saved to the database.
Setting Up the Database Connection
Now that we have our routes and data models defined, it's time to set up the database connection.
Set up your MongoDB database and save the connection url in your
.envfile.Run the following command to install the npm package
mongoose:
npm install --save mongoose
- Create a file called
db.jsin the/databasedirectory. In/database/db.js, set up the database connection using Mongoose:
const mongoose = require('mongoose');
const connect = (url) => {
mongoose.connect(url || 'mongodb://[localhost:27017](http://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 };
The connect function takes an optional url argument, which specifies the URL of the database to connect to. If no URL is provided, it defaults to 'mongodb://localhost:27017', which connects to the MongoDB instance running on the local machine at the default port (27017).
- Create an
index.jsfile in the/databasedirectory:
const database = require("./db");
module.exports = {
database,
};
Now that we have the database connection set up, in the next article, we'll dive into two important concepts- authentication and data validation. Stay tuned!
Top comments (0)