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.
Below are the tools and technologies we would be using for building our RESTful API
This guide assumes the following
If you don't have it set up, read this tutorial to set up PostgreSQL on an Alibaba Cloud Elastic Compute Service (ECS) instance.
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
- Stateless operations
- RESTful resource caching
- Use of a uniform interface.
Check on this link for more explanation on RESTful API conditions.
cd your-project-folder
npm init -y
This would create a package.json
file in our root directory.
run the command below to set up babel
npm install babel-preset-env --save-dev
npm install babel-cli --save
npm install babel-core --save
run touch .babelrc
to create a babel configuration file. and paste the code below
{
"presets": ["env"]
}
With babel setup, we can now create our RESTful API using ES6. To create our express application, we need to install express alongside some dependencies
npm install express body-parser morgan
Create a new file named app.js
to setup express
touch app.js
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}/`);
});
We need to install nodemon
to restart our server whenever we make changes to any of our file.
npm install --save-dev nodemon
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",
}
...
We are using nodemon to run the application and babel-node
to transpile our application from ES6
to ES5
on the run.
Now we can run our application with npm start
command.
With our application up and running, we need to install the sequelize library to connect to our postgreSQL database.
Install Sequelize, pg
(for making the database connection) and pg-hstore
(for serializing and deserializing JSON into the Postgres hstore key/value pair format):
npm install sequelize pg pg-hstore
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
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
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')
};
This sequelize configuration file is explain below
To create the files specified in the .sequelizerc
file, we are going to initialize sequelize by running sequelize init
.
sequelize init
After running sequelize init
command, Here is the structure of the files generated
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;
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.
We need to create our bookstore database. Run the command below to create a new database
createdb bookstore
createdb
command would be available once you have postgreSQL installed on your machine.
For the config.json
file in the server/config
directory, edit the file to fit the code below
{
"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"
}
}
For the purpose of this tutorial, we are only going to be using the development environment.
Now, we are going to create the models for our bookstore application and define the associations. Below is the schema for our bookstore. A schema is just a blueprint of how our database is being structured.
User model
To create our user model, run the command below
sequelize model:create --name User --attributes name:string,username:string,email:string,password:string
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');
}
};
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.
Let take a lot at the user model file user.js
generated in the server/models
directory
'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;
};
We are going to refactor this file to use `ES6` and add some validation for our user models as shown below.
export default (sequelize, DataTypes) => {
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING,
allowNull: {
args: false,
msg: 'Please enter your name'
}
},
username: {
type: DataTypes.STRING,
allowNull: {
args: false,
msg: 'Please enter your username'
}
},
email: {
type: DataTypes.STRING,
allowNull: {
args: false,
msg: 'Please enter your email address'
},
unique: {
args: true,
msg: 'Email already exists'
},
validate: {
isEmail: {
args: true,
msg: 'Please enter a valid email address'
},
},
},
password: {
type: DataTypes.STRING,
allowNull: {
args: false,
msg: 'Please enter a password'
},
validate: {
isNotShort: (value) => {
if (value.length < 8) {
throw new Error('Password should be at least 8 characters');
}
},
},
}
}, {});
User.associate = (models) => {
// associations can be defined here
};
return User;
};
We also need to update our user migration file to include the changes we made to our user model file.
Open the user migration file at server/migrations/<date>-create-user-.js
and update it to read the code below
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')
};
Book model
To create our user model, run the command below
sequelize model:create --name Book --attributes title:string,author:string,description:string,quantity:integer,userId:integer
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;
};
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;
};
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')
};
Association
Now, we need to define the associations between our user
and book
model. As a user, I should be able to have as many
books as possible while a book should belong to
a particular user. So our user model is going to have a One-to-many
relationship with the book model while our book model would have a many-to-one
relationship with the user model. You can check on the Sequelize docs
for more explanation on associations.
Edit the user.js
file in the server/models
directory to define the relationship between user
and book
as shown below
...
User.associate = (models) => {
// associations can be defined here
User.hasMany(models.Book, {
foreignKey: 'userId',
});
};
return User;
...
Also, edit the `book.js` file in the `server/models` directory to define the relationship between `book` and `user` as shown below
...
Book.associate = (models) => {
// associations can be defined here
Book.belongsTo(models.User, {
foreignKey: 'userId',
onDelete: 'CASCADE'
});
};
return User;
...
The onDelete: CASCADE
ensures whenever we delete a user, the books
associated with such user should also be deleted.
With the models and migrations in place, we can create those changes to the database by running the command below
Sequelize db:migrate
With our models and database in place, we are ready to create our controllers. We would be creating a user
and book
controllers, The controllers would be responsible for the CRUD(CREATE, READ, UPDATE and DELETE)
operations
For our user controller
controllers
folder in the server
directoryuser.js
file in the server/controllers
directory to define our user functionality which would also a user to create an account. 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;
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.
routes
folder in the server
directoryindex.js
file in the server/routes
directory. This is where we are going to define our API endpoints. Paste the code below 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
};
In this file, we are importing our Users
class and defining two API endpoints.
/api
endpoint has an HTTP Method of GET
which can be translated as READ
in the CRUD operation./api/users
endpoint has an HTTP Method of POST
which can be translated as CREATE
in the CRUD operation. Whenever we hit this API endpoint, we are calling the signUp
method from our Users
class which is going to create a new user.app.js
file and edit it to look like this 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}/`);
});
Postman
and create a new user as shown below
Note when building either a production or development ready API, you are to encrypt the password
value using packages like bcrypt. You should see the user data you created just now in your database.
create a new file named book.js
in the server/controllers
directory and paste the code below
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
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
};
Note that userId
is the Id of user we created earlier
We would modify our book controller to enable us to get the list of all the books in our database
book.js
in the server/controllers
directory and update it to include this ...
static list(req, res) {
return Book
.findAll()
.then(books => res.status(200).send(books));
}
...
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
...
We would modify our book controller to allow us to modify a book data in our database
book.js
in the server/controllers
directory and update it to include this ...
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));
}
...
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
...
bookId
is the id of the book to be edited
Finally, we are going to add a functionality to delete a book
book.js
in the server/controllers
directory and update it to include this ...
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))
}
...
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
...
For reference purposes, The complete code for this article can be found on this Github Repository
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
2,599 posts | 762 followers
FollowHironobu Ohara - May 25, 2023
Alibaba Clouder - May 6, 2019
Alibaba Clouder - March 18, 2019
Alibaba Clouder - March 8, 2019
ApsaraDB - October 8, 2024
Alex - October 16, 2018
2,599 posts | 762 followers
FollowElastic and secure virtual cloud servers to cater all your cloud hosting needs.
Learn MoreAn encrypted and secure cloud storage service which stores, processes and accesses massive amounts of data from anywhere in the world
Learn MoreLearn More
More Posts by Alibaba Clouder
Ravi Kumar Singh April 24, 2019 at 9:46 am
This is awesome example. Works really fine. Just some updates require that in the new versions of sequelize findById has been replaced by findByPk. Overall its a great article.