Building CRUD API using Restify Framework
A lot of countless options exist for building APIs with JavaScript frameworks these days, and each with their own advantages.
This article will focus on explaining the steps to build an API with Create Read Update and Delete (CRUD) functions using the Restify Framework for Node.js.
The project we’re going to be building would be a mini MVP loan management API that enables you to Create Read Update and Delete debtors with authentication implemented using JSON Web Tokens (JWT).
Let’s get started.
Creating the Project
create a folder called “debtor-api
” in your preferred directory then open up the folder in your favorite terminal and run the following command.
npm init -y
You should get a response that says:
Wrote to /path_to/debtor-api/package.json:
{
"name": "debtor-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Open up the folder in your preferred text editor (eg VS Code) and modify the package.json
file to run nodemon
, a package that watches your project for changes and automatically reloads your server (we’ll install this later). In order to do this, replace the contents of the package.json file with the code below:
{
"name": "debtor-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {}
}
Next, run the below commands to install nodemon (adds live reload to the dev server), restify (the framework we’re working with), mongoose (an elegant MongoDB object modeling for nodejs), restify-errors (helps to handle errors elegantly) and mongoose-timestamps (adds a timestamps field to our collections).
npm i nodemon restify restify-errors mongoose mongoose-timestamp
Next, create a MongoDB database either locally or on the cloud with MongoDB Atlas and fetch your connection string.
Setting up configuration
Create a new file in your project root directory and call it config.js
and put the following settings in the file:
module.exports = {
ENV: process.env.NODE_ENV || 'development',
PORT: process.env.PORT || 5000,
URL: process.env.BASE_URL || 'http://localhost:5000',
MONGODB_URI: 'mongodb//user:password@hosturl/databasename'
}
Make sure to replace the value of MONGODB_URI
above with your connection string.
Setting up the server
Create a file called index.js in your root folder and write the following code:
const restify = require('restify');
const mongoose = require('mongoose');
const config = require('./config');
const rjwt = require('restify-jwt-community');
const server = restify.createServer();
//Middleware
server.use(restify.plugins.bodyParser())
//Protected Routes
/*
the line below helps protect all routes from unauthorized access
i.e You need a token to perform actions on all routes except those in the unless({{path:[]}) array
*/
server.use(rjwt({ secret: config.JWT_SECRET }).unless({ path: ['/api/auth', '/api/debtors/','/api/debtors:id'] }));
server.listen(config.PORT, () => {
mongoose.set('useFindAndModify', false)
mongoose.connect(
config.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}
);
});
const database = mongoose.connection;
database.on('error', (err) => {
console.log(err)
})
database.once('open', () => {
require('./routes/debtors')(server)
require('./routes/users')(server)
console.log(`Server running on Port: ${config.PORT}`);
})
Setting up the Debtor
and User
models
In your project folder create two subfolders called models and routes.
In the models
‘ subfolder, make a new file called Debtor.js (make sure it starts with a capital D) and put the following code into it:
const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const DebtorSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
debt: {
type: Number,
required: false,
default: 0
},
phone: {
type: String,
required: false,
default: ''
}
});
DebtorSchema.plugin(timestamp) //adds timestampsautomatically to the model in the database
const Debtor = mongoose.model('Debtor', DebtorSchema);
module.exports = Debtor; //exports the Debtor model for reuse
The code block above defines a mongoose schema called DebtorSchema it also specifies the model to have the following fields: name
, email
, debt
and phone
as well as timestamps (created_at
and updated_at
) fields whose creation and maintenance will be handled by mongoose automatically.
Next, create a similar file for our User model in the same models
folder and name it User.js (uppercase U) then add the following code to create a UserSchema with email
and password
fields:
//models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true
}
});
module.exports = mongoose.model('User', UserSchema); // exports the User model for reuse in other files
Adding Routes to the API
Let’s add routes and configure the methods allowed and then the actions we want those routes to perform.
In your root folder again, create a new folder named routes
and inside the folder, create two files: debtors.js
and users.js
. In the debtors.js
file put the following block of code:
const errors = require('restify-errors')
const Debtor = require('../models/Debtor');
module.exports = server => {
// Fetch All Debtors
server.get('/api/debtors/', async (req, res, next) => {
try {
const debtors = await Debtor.find({}) //query to look for all debtors
res.send(debtors); // send the result from the query above back to the user
next();
} catch (error) {
return next(new errors.InvalidContentError(error))
}
});
// Add a new Debtor to list
server.post('/api/debtors', async (req, res, next) => {
// Check if what is being passed is in JSON
if (!req.is('application/json')) {
return next(new errors.InvalidContentError("This API expects: 'application/json'"))
}
const { name, email, phone, debt } = req.body;
const debtor = new Debtor({
name,
email,
phone,
debt
});
try {
const newDebtor = await debtor.save() //save a debtor to the database
res.send(201) //201 means created
next();
} catch (error) {
return next(new errors.InternalError(error.message))
}
});
//Fetch a Single Debtor
server.get('/api/debtors:id', async (req, res, next) => {
try {
const debtor = await Debtor.findById(req.params.id)
res.send(debtor);
next();
} catch (error) {
return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`))
}
});
// Update The Details of an Exising Debtor
server.put('/api/debtors:id', async (req, res, next) => {
// Check if what is being passed is in JSON
if (!req.is('application/json')) {
return next(new errors.InvalidContentError("This API expects: 'application/json'")) // data must be in json format
}
try {
const debtor = await Debtor.findOneAndUpdate({_id: req.params.id}, req.body)
res.send(200)
next();
} catch (error) {
return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`)) // this is called when a debtor with the ID does not exist
}
}
});
//Delete an Already Existing Debtor
server.del('/api/debtors:id', async (req, res, next) => {
//restify uses 'server.del' instead of 'server.delete' for delete operations
try {
const debtor = await Debtor.findOneAndRemove({ _id: req.params.id }) // await a query to look for debtor with the given id
res.send(204); // 204 response means no content
next()
} catch (error) {
return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`)) //this is called when a debtor with the ID does not exist
}
})
}
Adding Authentication with JWT
To set up authentication for our application, we need a few npm packages namely: bcryptjs
, restify-jwt-community
and jsonwebtoken
. Install them by running the command below
npm install restify-jwt-community jsonwebtoken bcryptjs
After the packages above have been installed, add the following property to the modules.export
object in the config.js file you created earlier and replace ‘supersecretkey
‘ with anything of your choice (Note: do not leave such exposed in a production environment’):
JWT_SECRET: process.env.JWT_SECRET || 'supersecretkey'
Create another file called auth.js
in your routes folder and add the following code:
const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
const User = mongoose.model('User');
exports.authenticate = (email, password) => {
return new Promise(async (resolve, reject) => {
try {
//try to fetch user
const user = await User.findOne({ email })
// Match User email with password
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
resolve(user);
} else {
// Password did not match
reject('Failed to authenicate user')
}
})
} catch (err) {
// Can't find user email
reject('Sorry, Authentication failed')
}
});
}
Next, in the routes/users.js
file add the following to set up the user routes and API methods:
const errors = require('restify-errors');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const auth = require('../routes/auth')
const jwt = require('jsonwebtoken');
const config = require('../config')
module.exports = server => {
// Create a new User
server.post('/auth/register', (req, res, next) => {
const { email, password } = req.body;
const user = new User({
email,
password
});
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(user.password, salt, async (err, hash) => {
// Encrypt the password
user.password = hash;
//Save User to database
try {
const newUser = await user.save();
res.send(201)
next()
} catch (err) {
return next(new errors.InternalError(err.message))
}
});
})
});
//Authenticate a User
server.post('/api/auth', async (req, res, next)=> {
const { email, password } = req.body;
try {
const user = await auth.authenticate(email, password);
// Create JWT
const token = jwt.sign(user.toJSON(), config.JWT_SECRET, { expiresIn: '10m' })
const { iat, exp } = jwt.decode(token);
// respond with token
res.send({iat, exp, token})
console.log(iat, exp, token)
next();
} catch(error) {
//Unauthorized
return next(new errors.UnauthorizedError(error))
}
})
}
Running the App
To test-run our API open up your terminal and either of the following command:
npm run dev
#or
nodemon index.js
Next, head over to Postman or any API testing tool of your choice and make requests to the API endpoints and you should get responses similar to these below:
Conclusion:
Restify framework provides a fast and efficient way to build REST APIs using Nodejs and JavaScript and is fast becoming one of the most popular API development frameworks in existence today.
A flaw, however (in my opinion) is that it is not as MVC based (templating and rendering are not supported out-of-the-box) as other frameworks like Adonis, Express, Loopback.js, etc hence, you would want to integrate a frontend framework to handle the View rendering.
You can check out the source code for this tutorial on Github