How to create a node.js api server using typescript
Dallington Asingwire
Posted on August 29, 2023
Noe.js is a popular runtime environment for building server-side applications include APIs (Application Programming Interfaces).
In this tutorial, we will see how to create a RESTful API in node.js using typescript and mysql.
Follow these steps to setup your project;
- Create project folder and navigate into it.
- Run
npm init -y
to initialize a new node.js project. - Install project dependencies
npm i body-parser dotenv express joi jsonwebtoken morgan mysql2 sequelize bcrypt
- body-parser; allows you to extract data from the request body.
- dotenv; allows you to load environment variables from .env file.
- express; provides small and robust tools for HTTP servers like routing, middleware setup etc.
- joi; allows you to define validation schemas and validate data types like strings, numbers, arrays etc.
- jsonwebtoken; used for authenticating/authorizing requests in your node.js application.
- morgan; logs details about the incoming HTTP requests e.g request method, URL, status code etc.
- mysql2; mysql client for node.js applications.
- sequelize; provides a convenient way to interact with relational databases e.g MySQL, PostgreSQL etc.
- bcrypt; helps you hash passwords.
4.To use typescript in node.js, install typescript as a development dependency in your project. The command below installs dev dependencies for typescript, node and Express.js
npm i --save-dev typescript@5.1.6 @types/node @types/express
5.Create tsconfig.json file in the root of your project directory. This file contains typescript compile options and configuration settings.
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
6.Setup project structure
Setup your project files and sub folders as shown below;
-
src
common
config
constants
controllers
database
interfaces
middleware
routes
services
.env
.gitignore
package.json
package-lock.json
tsconfig.json
7.Create a file env.ts under constants folder.
require('dotenv').config()
const envConstants = {
ENVIRONMENT: process.env.ENVIRONMENT,
PRIVATE_AUTH_SECRET: process.env.PRIVATE_AUTH_SECRET,
DB: {
LOCAL: {
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT
},
DEV: {
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT
},
PROD: {
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT
},
},
};
export { envConstants }
This file loads environment variables from the .env file
8.Create env.ts file under the config folder. This file loads environment variables for a specific environment.
import { envConstants as env } from "../constants/env"
process.env.NODE_ENV = env.ENVIRONMENT;
module.exports = () => {
switch (process.env.NODE_ENV) {
case "local":
return {
DB: env.DB.LOCAL,
};
case "dev":
return {
DB: env.DB.DEV,
};
case "production":
return {
DB: env.DB.PROD,
};
default:
return {
DB: env.DB.LOCAL,
};
}
};
9.Create a file connection.ts under database connection
const env = require('../config/env')();
const Sequelize = require('sequelize');
const sequelize = new Sequelize(
env.DB.name,
env.DB.user,
env.DB.password,
{
host: env.DB.host,
dialect: env.DB.dialect,
logging: false,
dialectOptions: {
multipleStatements: true,
connectTimeout: 150000
},
pool: {
max: 350,
min: 0,
acquire: 220000,
idle: 10000
}
});
const connectDB = () => {
sequelize.authenticate()
.then(() => {
sequelize.sync({ alter: false });
console.log('Connection has been established successfully.');
})
.catch((err: unknown) => {
console.error('Unable to connect to the database:', err);
});
};
const dbCredentails = {
username: env.DB.user,
password: env.DB.password,
database: env.DB.name,
host: env.DB.host,
dialect: env.DB.dialect,
dialectOptions: {
multipleStatements: true,
connectTimeout: 60000
},
}
export {
connectDB,
sequelize,
dbCredentails
}
This file establishes database connection.
NOTE: The api server we are creating will perform CRUD on the user i.e creating, reading, updating and deleting user from the mysql database.
So let's create model User, service UserService, userController and then endpoints for creating, fetching, updating and deleting users.
10.Create User Model under a subfolder models under database folder.
import { Sequelize, Model, DataTypes, Optional } from 'sequelize';
import { UserAttributes } from '../../interfaces';
interface UserCreationAttributes
extends Optional<UserAttributes, 'id'> { }
class User extends Model<UserAttributes, UserCreationAttributes>
implements UserAttributes {
public id!: number;
public firstName!: string;
public lastName!: string;
public phoneNumber!: string;
public email!: string;
public address!: string;
public password?: string;
public isDeleted!: boolean;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
static initialize(sequelize: Sequelize) {
this.init(
{
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
firstName: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
lastName: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
phoneNumber: {
type: DataTypes.STRING(15),
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING(40),
allowNull: false,
unique: true
},
address: {
type: DataTypes.STRING(30),
allowNull: false
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
isDeleted: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
sequelize: sequelize,
modelName: 'users',
tableName: 'users',
timestamps: true,
}
);
}
}
export { User }
The above User model uses UserAttributes interface from the interfaces folder. So create that interface in a index.ts file under interfaces folder as shown below;
interface UserAttributes {
id: number;
firstName: string;
lastName: string;
phoneNumber: string;
email: string;
address: string;
password?: string;
isDeleted: boolean;
}
export { UserAttributes }
Now that we have created User model, let's create UserService under services folder that has methods for creating, inserting and updating user data in the database.
import { User } from '../database/models/User'
import { sequelize as db } from "../database/connection"
User.initialize(db)
class UserService {
public get(criteria: any = {}, projection: any = []) {
let config: any = { }
if (criteria.length > 0) {
config.criteria = criteria;
}
if (projection.length > 0) {
config.attributes = projection;
}
return new Promise((resolve, reject) => {
User.findAll(config).then(result => {
resolve(result)
}).catch(function (err) {
console.log("Error on fetching users", err)
reject(err)
})
})
}
public create(data: User) {
return new Promise((resolve, reject) => {
User.create(data).then(function (obj: any) {
resolve(obj)
}).catch(function (err) {
console.log("Error on inserting user", err)
reject(err)
})
})
}
public insertorUpdate(criteria: any, data: User) {
return new Promise((resolve, reject) => {
User.findOne({
where: criteria
}).then(function (obj: any) {
if (obj) {
obj = obj.update(data)
} else {
obj = User.create(data)
}
resolve(obj)
}).catch(function (err) {
console.log("Error on inserting or updating user", err)
reject(err)
})
})
}
public update(criteria: any, objToSave: Partial<User>) {
return new Promise((resolve, reject) => {
User.update(objToSave,
{ where: criteria })
.then(result => {
resolve(result)
}).catch(function (err) {
console.log("Error on updating user", err)
reject(err)
})
})
}
public find = (criteria: any) => {
return new Promise((resolve, reject) => {
User.findOne({
where: criteria
}).then(result => {
resolve(result)
}).catch(function (err) {
console.log("Error on fetching user details", err)
reject(err)
})
})
}
public count(criteria: any) {
return new Promise((resolve, reject) => {
User.count({
where: criteria
}).then(result => {
resolve(result)
}).catch(err => {
console.log("Error in getting count for users", err)
reject(err)
})
})
}
}
export { UserService }
UserController receives requests from routes and processes them by sending requests to the above service for data processing in the database.
import { verifyPayload, hashPassword, generateRandomStr } from '../common'
import { Request } from 'express'
import { UserService } from '../services/UserService'
const Joi = require('joi')
class UserController {
userService: UserService
constructor(){
this.userService = new UserService()
}
async get() {
try {
const projection = ["id", "firstName", "lastName", "phoneNumber", "email", "address", "roleId", "departmentId"]
const users = await this.userService.get({}, projection);
return users
} catch (err) {
throw err
}
}
async find(req: Request) {
try {
const schema = Joi.object().keys({
id: Joi.number().required()
})
const payload = await verifyPayload(req.params, schema)
if (payload.error == null) {
const id = payload.id
const criteria = { id: id }
const data = await this.userService.find(criteria)
return data
}
} catch (err) {
throw err
}
}
async create(req: Request) {
try {
const schema = Joi.object().keys({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
phoneNumber: Joi.string().required(),
email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } }).required(),
address: Joi.string().required(),
roleId: Joi.number().required(),
departmentId: Joi.number().required()
})
const payload = await verifyPayload(req.body, schema)
if (payload.error == null) {
const password = generateRandomStr()
const hashedPassword = await hashPassword(password)
const data = {
firstName: payload.firstName,
lastName: payload.lastName,
phoneNumber: payload.phoneNumber,
email: payload.email,
address: payload.address,
roleId: payload.roleId,
departmentId: payload.departmentId,
password: hashedPassword
}
const result = await this.userService.create(data)
return result
}
} catch (err) {
throw err
}
}
async update(req: Request) {
try {
const querySchema = Joi.object().keys({
id: Joi.number().required()
})
const payloadSchema = Joi.object().keys({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
phoneNumber: Joi.string().required(),
email: Joi.string().required(),
address: Joi.string().required(),
roleId: Joi.number().required(),
departmentId: Joi.number().required()
})
const queryBody = await verifyPayload(req.params, querySchema)
const payload = await verifyPayload(req.body, payloadSchema)
if (payload.error == null && queryBody.error == null) {
const criteria = { id: queryBody.id }
const data = {
firstName: payload.firstName,
lastName: payload.lastName,
phoneNumber: payload.phoneNumber,
email: payload.email,
address: payload.address,
roleId: payload.roleId,
departmentId: payload.departmentId
}
const result = await this.userService.update(criteria, data)
return result
}
} catch (err) {
throw err
}
}
async delete(req: Request) {
try {
const querySchema = Joi.object().keys({
id: Joi.number().required()
})
const queryBody = await verifyPayload(req.params, querySchema)
if (queryBody.error == null) {
const criteria = { id: queryBody.id }
const data = {
isDeleted: true
}
const result = await this.userService.update(criteria, data)
return result
}
} catch (err) {
throw err
}
}
}
export { UserController }
User controller uses reusable methods i.e verifyPayload, hashPassword and generateRandomStr from index.ts file under common folder in the project root directory and is as follows;
import { Response } from "express"
import { apiConstants } from '../constants/api'
import { ApiResponse } from "../interfaces"
const bcrypt = require("bcrypt")
const sendSuccessMessage = (res: Response, data: any, message: string) => {
const response: ApiResponse = {
statusCode: apiConstants.SUCCESS_STATUS_CODE,
message: message,
data: data || {}
};
res.status(apiConstants.SUCCESS_STATUS_CODE).send(response);
}
const sendErrorMessage = (res: Response, error: any) => {
let message = error.message;
if (error.isJoi) {
message = error.details[0].message;
}
const response: ApiResponse = {
statusCode: apiConstants.ERROR_STATUS_CODE,
message: message
}
res.status(apiConstants.ERROR_STATUS_CODE).send(response);
}
const verifyPayload = async (request: any, requestSchema: any) => {
try {
const value = await requestSchema.validateAsync(request);
return value;
} catch (error) {
throw error;
}
}
const generateRandomStr = () => {
return (Math.random() + 1).toString(36).substring(7)
}
const hashPassword = async (plaintextPassword: string) => {
const hash = await bcrypt.hash(plaintextPassword, 15);
return hash
}
export { sendSuccessMessage, sendErrorMessage, verifyPayload, generateRandomStr, hashPassword}
The index.ts in common folder uses apiConstants defined in the api.ts file under constants folder and is as follows;
const apiConstants = {
SUCCESS_STATUS_CODE: 200,
ERROR_STATUS_CODE: 400,
SUCCESS_STATUS_TEXT: 'SUCCESS',
ERROR_STATUS_TEXT: 'ERROR',
SUCCESS_STATUS_MESSAGE: 'OK',
ERROR_STATUS_MESSAGE: 'FAILED',
}
export { apiConstants }
Previously, we had created interface UserAttributes in the index.ts under interfaces, add ApiResponse interface which is being used by sendSuccessMessage and sendErrorMessage methods. We shall see these methods being used by routes.
Now that we have UserController, lets create user routes, create a file user.ts under routes folder and is as follows;
import { Request, Response } from "express"
const express = require('express')
const router = express.Router()
import { UserController } from '../controllers/UserController'
import { sendSuccessMessage, sendErrorMessage } from '../common'
const controller = new UserController
router.get("/", async (req: Request, res: Response) => {
try {
const data = await controller.get()
sendSuccessMessage(res, data, 'success')
} catch (error: unknown) {
sendErrorMessage(res, error)
}
})
router.post("/", async (req: Request, res: Response) => {
try {
const data = await controller.create(req)
sendSuccessMessage(res, data, 'success')
} catch (error: unknown) {
sendErrorMessage(res, error)
}
})
router.put("/:id", async (req: Request, res: Response) => {
try {
const data = await controller.update(req)
sendSuccessMessage(res, data, 'success')
} catch (error: unknown) {
sendErrorMessage(res, error)
}
})
router.delete("/:id", async (req: Request, res: Response) => {
try {
const data = await controller.delete(req)
sendSuccessMessage(res, data, 'success')
} catch (error: unknown) {
sendErrorMessage(res, error)
}
})
module.exports = router
Our next task is to create the server so create the file app.ts within the src folderas follows.
import express from 'express'
import { connectDB } from './database/connection'
const logger = require('morgan')
const bodyParser = require('body-parser')
require('./database/models')
const app = express();
const userRoutes = require('./routes/user')
//Setting the hostname and port number for the server to listen
const hostname = "127.0.0.1";
const port = 3000;
// database setup
connectDB()
app.use(logger('dev'))
app.use(express.json())
app.use(bodyParser.urlencoded({
limit: '100mb',
extended: true,
parameterLimit: 1000000
}))
app.get('/', (req, res) => {
res.send('Node.js API Server!');
});
app.use('/api/v1/users', userRoutes)
app.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}`);
});
Create .env file in your project root directory
DB_HOST=localhost
DB_NAME=testdb
DB_USER=root
DB_PASSWORD=root
DB_DIALECT=mysql
Adjust your package.json file to include the following scripts;
"scripts": {
"compile": "npx tsc",
"start": "tsc && node dist/app.js",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
Finally, add .gitignore to specify files and directories that should be ignored and not tracked by Git and in our project, this is how it looks like.
node_modules
dist
.env
Testing API with ThunderClient (You can use Postman too)
1. Fetch all users
http://127.0.0.1:3000/api/v1/users
2. Create user
3. Update user
Conclusion
We have created a node.js RESTful api server using typescript. In the next article, we will see how to secure our endpoints using authentication middleware. Thank you for attending today's class.👏🏻😅
Github link: 👉 https://github.com/DallingtonAsin/nodejs-api-server.git
Posted on August 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024