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.js
and 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.js
and 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
.env
file.Run the following command to install the npm package
mongoose
:
npm install --save mongoose
- Create a file called
db.js
in the/database
directory. 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.js
file in the/database
directory:
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)