Building a RESTful API with Express, PostgreSQL, and Node Using ES6

By Sanni Kehinde, Alibaba Cloud Tech Share Author. Tech Share is Alibaba Cloud’s incentive program to encourage the sharing of technical knowledge and best practices within the cloud community.

In this guide, I would be explaining how to build a basic bookstore RESTful API where a user can perform a basic CRUD (CREATE, READ, UPDATE AND DELETE) operation.

Tools

Below are the tools and technologies we would be using for building our RESTful API

  1. NodeJS — Node.js is an open-source, cross-platform JavaScript run-time environment that executes JavaScript code outside of a browser. Basically, we would be using this to run our javascript code on the server.

Prerequisites

This guide assumes the following

  1. You already have a basic knowledge of JavaScript

If you don’t have it set up, read this tutorial to set up PostgreSQL on an Alibaba Cloud Elastic Compute Service (ECS) instance.

What’s RESTful API?

A RESTful API also referred to as RESTful web service and is based on representational state transfer(REST) technology, an architectural style and approach to communications often used in web services development. It’s an application program interface (API) that uses HTTP requests such as GET, PUT, POST and DELETE methods on data.

While most API’s claim to be RESTful, it’s important to know that there are some conditions that determine if your API is RESTful which are listed below

  • Client-Server-based

Check on this link for more explanation on RESTful API conditions.

Getting Started

  1. To get started with building our RESTful API, we need to create a directory for our project. Move into the new directory and initialize NodeJS by running the command below
  • cd your-project-folder npm init -y
  1. This would create a package.json file in our root directory.
  • npm install babel-preset-env --save-dev npm install babel-cli --save npm install babel-core --save
  1. run touch .babelrc to create a babel configuration file. and paste the code below
  • { "presets": ["env"] }
  1. Creating our express application
  • npm install express body-parser morgan
  1. Create a new file named app.js to setup express
  • touch app.js
  1. and paste the code below
  • import http from 'http'; import express from 'express'; import logger from 'morgan'; import bodyParser from 'body-parser'; const hostname = '127.0.0.1'; const port = 3000; const app = express() // setup express application const server = http.createServer(app); app.use(logger('dev')); // log requests to the console // Parse incoming requests data app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.get('*', (req, res) => res.status(200).send({ message: 'Welcome to the default API route', })); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
  1. We need to install nodemon to restart our server whenever we make changes to any of our file.
  • npm install --save-dev nodemon
  1. To use nodemon, open the package.json file and update the scripts section to the code below
  • ... "scripts": { "start": "nodemon --exec babel-node app.js", } ...
  1. We are using nodemon to run the application and babel-node to transpile our application from ES6to ES5 on the run.
  • npm install sequelize pg pg-hstore
  1. We need to install the sequelize CLI which enable us to run database migration easily from the terminal and bootstrap a new project.
  • npm install -g sequelize-cli
  1. Next, we are going to create a config file in our root directory for sequelize named .sequelizerc. Basically, In this file, we are telling sequelize where to find to it's required files.
  • touch .sequelizerc
  1. and paste the code below
  • const path = require('path'); module.exports = { "config": path.resolve('./server/config', 'config.json'), "models-path": path.resolve('./server/models'), "seeders-path": path.resolve('./server/seeders'), "migrations-path": path.resolve('./server/migrations') };
  1. This sequelize configuration file is explain below
  • Config: This file contains our application configuration settings such as database configuration.
  1. To create the files specified in the .sequelizerc file, we are going to initialize sequelize by running sequelize init.
  • sequelize init
  1. After running sequelize init command, Here is the structure of the files generated
  1. Let take a look at the index.js file generated in the server/models directory
  • 'use strict'; var fs = require('fs'); var path = require('path'); var Sequelize = require('sequelize'); var basename = path.basename(__filename); var env = process.env.NODE_ENV || 'development'; var config = require(__dirname + '/../config/config.json')[env]; var db = {}; if (config.use_env_variable) { var sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { var sequelize = new Sequelize(config.database, config.username, config.password, config); } fs .readdirSync(__dirname) .filter(file => { return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); }) .forEach(file => { var model = sequelize['import'](path.join(__dirname, file)); db[model.name] = model; }); Object.keys(db).forEach(modelName => { if (db[modelName].associate) { db[modelName].associate(db); } }); db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db;
  1. So in this file, we establish a connection to the database, grab all the model files from the current directory, add them to the db object, and apply any relations between each model (if any). This file uses development environment by default if NODE_ENV is not specified.
  • createdb bookstore
  1. createdb command would be available once you have postgreSQL installed on your machine.
  • { "development": { "username": "your_database_username", "password": "your_database_password", "database": "bookstore", "host": "127.0.0.1", "dialect": "postgres" }, "test": { "username": "root", "password": null, "database": "database_test", "host": "127.0.0.1", "dialect": "postgres" }, "production": { "username": "root", "password": null, "database": "database_production", "host": "127.0.0.1", "dialect": "postgres" } }
  1. For the purpose of this tutorial, we are only going to be using the development environment.
  1. User model
  • sequelize model:create --name User --attributes name:string,username:string,email:string,password:string
  1. A new user migration file would be created in the server/migration directory as shown below
  • 'use strict'; module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('Users', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, name: { type: Sequelize.STRING }, username: { type: Sequelize.STRING }, email: { type: Sequelize.STRING }, password: { type: Sequelize.STRING }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('Users'); } };
  1. When we run our migration, which we are going to do later in this section, the up function would be executed and creates the table and associated columns for us in our database. whenever we want to undo such changes the down function would be executed when we run the sequelize db:migrate:undo:all command.
  • 'use strict'; module.exports = (sequelize, DataTypes) => { var User = sequelize.define('User', { name: DataTypes.STRING, username: DataTypes.STRING, email: DataTypes.STRING, password: DataTypes.STRING, }, {}); User.associate = function(models) { // associations can be defined here }; return User; };
  1. We also need to update our user migration file to include the changes we made to our user model file.
  • module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('Users', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, name: { allowNull: false, type: Sequelize.STRING }, username: { allowNull: false, type: Sequelize.STRING }, email: { allowNull: false, unique: true, type: Sequelize.STRING }, password: { type: Sequelize.STRING }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: queryInterface /* , Sequelize */ => queryInterface.dropTable('Users') };
  1. Book model
  • sequelize model:create --name Book --attributes title:string,author:string,description:string,quantity:integer,userId:integer
  1. A book model file book.js is generated in the server/model directory as shown below
  • 'use strict'; module.exports = (sequelize, DataTypes) => { var Book = sequelize.define('Book', { title: DataTypes.STRING, author: DataTypes.STRING, description: DataTypes.STRING, quantity: DataTypes.INTEGER, userId: DataTypes.INTEGER }, {}); Book.associate = function(models) { // associations can be defined here }; return Book; };
  1. We would also update this file to use ES6 and add some validations for our book model
  • export default (sequelize, DataTypes) => { const Book = sequelize.define('Book', { title: { type: DataTypes.STRING, allowNull: { args: false, msg: 'Please enter the title for your book' } }, author: { type: DataTypes.STRING, allowNull: { args: false, msg: 'Please enter an author' } }, description: { type: DataTypes.STRING, allowNull: { args: false, msg: 'Pease input a description' } }, quantity: { type: DataTypes.INTEGER, allowNull: { args: false, msg: 'Pease input a quantity' } }, userId: { type: DataTypes.INTEGER, references: { model: 'User', key: 'id', as: 'userId', } } }, {}); Book.associate = (models) => { // associations can be defined here }; return Book; };
  1. We are also going to update the books migration file at server/migrations/<date>-create-book-.js to include the changes made to the book model.
  • module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('Books', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, title: { allowNull: false, type: Sequelize.STRING }, author: { allowNull: false, type: Sequelize.STRING }, description: { allowNull: false, type: Sequelize.STRING }, quantity: { allowNull: false, type: Sequelize.INTEGER }, userId: { type: Sequelize.INTEGER, onDelete: 'CASCADE', references: { model: 'Users', key: 'id', as: 'userId', } }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: queryInterface /* , Sequelize */ => queryInterface.dropTable('Books') };
  1. Association
  • ... User.associate = (models) => { // associations can be defined here User.hasMany(models.Book, { foreignKey: 'userId', }); }; return User; ...
  1. The onDelete: CASCADE ensures whenever we delete a user, the books associated with such user should also be deleted.
  • Sequelize db:migrate
  1. Controllers

For our user controller

  1. Create a controllers folder in the server directory
  • import model from '../models'; const { User } = model; class Users { static signUp(req, res) { const { name, username, email, password } = req.body return User .create({ name, username, email, password }) .then(userData => res.status(201).send({ success: true, message: 'User successfully created', userData })) } } export default Users;
  1. Basically, we are importing our models object and then use object destructuring to get our user model. In our Users class, we create a method called signUp which is responsible for creating our user.
  • import Users from '../controllers/user'; export default (app) => { app.get('/api', (req, res) => res.status(200).send({ message: 'Welcome to the BookStore API!', })); app.post('/api/users', Users.signUp); // API route for user to signup };
  1. In this file, we are importing our Users class and defining two API endpoints.
  • import http from 'http' import express from 'express' import logger from 'morgan'; import bodyParser from 'body-parser'; import routes from './server/routes'; const hostname = '127.0.0.1'; const port = 3000; const app = express() const server = http.createServer(app); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); routes(app); app.get('*', (req, res) => res.status(200).send({ message: 'Welcome to the .', })); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
  1. To test our API endpoint, Open up Postman and create a new user as shown below
  1. Note when building either a production or development ready API, you are to encrypt the passwordvalue using packages like bcrypt. You should see the user data you created just now in your database.

For our book controller

  1. Creating a book
  • import model from '../models'; const { Book } = model; class Books { static create(req, res) { const { title, author, description, quantity } = req.body const { userId } = req.params return Book .create({ title, author, description, quantity, userId }) .then(book => res.status(201).send({ message: `Your book with the title ${title} has been created successfully `, book })) } } export default Books
  1. We need to create an API endpoint for creating a book. To do so, Open the index.js file in the server/routes directory and update the code to read this
  • import Users from '../controllers/user'; import Books from '../controllers/book'; export default (app) => { app.get('/api', (req, res) => res.status(200).send({ message: 'Welcome to the bookStore API!', })); app.post('/api/users', Users.signUp); // API route for user to signup app.post('/api/users/:userId/books', Books.create); // API route for user to create a book };
  1. Note that userId is the Id of user we created earlier
  1. Listing all books
  • ... static list(req, res) { return Book .findAll() .then(books => res.status(200).send(books)); } ...
  1. Update the index.js file in the server/routes directory to define our API for listing all books.
  • ... app.get('/api/books', Books.list); // API route for user to get all books in the database ...
  1. Open up postman and test the new route
  1. Updating a book
  • ... static modify(req, res) { const { title, author, description, quantity } = req.body return Book .findById(req.params.bookId) .then((book) => { book.update({ title: title || book.title, author: author || book.author, description: description || book.description, quantity: quantity || book.quantity }) .then((updatedBook) => { res.status(200).send({ message: 'Book updated successfully', data: { title: title || updatedBook.title, author: author || updatedBook.author, description: description || updatedBook.description, quantity: quantity || updatedBook.quantity } }) }) .catch(error => res.status(400).send(error)); }) .catch(error => res.status(400).send(error)); } ...
  1. Update the index.js file in the server/routes directory to define our API endpoint for editing a books.
  • ... app.put('/api/books/:bookId', Books.modify); // API route for user to edit a book ...
  1. bookId is the id of the book to be edited
  1. Deleting a book
  • ... static delete(req, res) { return Book .findById(req.params.bookId) .then(book => { if(!book) { return res.status(400).send({ message: 'Book Not Found', }); } return book .destroy() .then(() => res.status(200).send({ message: 'Book successfully deleted' })) .catch(error => res.status(400).send(error)); }) .catch(error => res.status(400).send(error)) } ...
  1. Update the index.js file in the server/routes directory to define our API endpoint for editing a books.
  • ... app.delete('/api/books/:bookId', Books.delete); // API route for user to delete a book ...
  1. Open up postman and test the new route
  1. For reference purposes, The complete code for this article can be found on this Github Repository

Conclusion

Finally, we have come to the end of this article. This article is just a basic of getting started with RESTful API. We were able to create a basic CRUD operation, but here are some few things you could try out on your own

  1. Encrypt the password field using packages like bcrypt

Reference:https://www.alibabacloud.com/blog/building-a-restful-api-with-express%2C-postgresql%2C-and-node-using-es6_594137?spm=a2c41.12245160.0.0

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.