DEV Community

Dallington Asingwire
Dallington Asingwire

Posted on

How to create a node.js api server using typescript

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;

  1. Create project folder and navigate into it.
  2. Run npm init -y to initialize a new node.js project.
  3. Install project dependencies
npm i body-parser dotenv express joi jsonwebtoken morgan mysql2 sequelize bcrypt
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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,
            };
    }
};
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});

Enter fullscreen mode Exit fullscreen mode

Create .env file in your project root directory

DB_HOST=localhost
DB_NAME=testdb
DB_USER=root
DB_PASSWORD=root
DB_DIALECT=mysql
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing API with ThunderClient (You can use Postman too)

1. Fetch all users
http://127.0.0.1:3000/api/v1/users

Get all users endpoint

2. Create user

Create user endpoint

3. Update user

Update user endpoint
4. Delete user

Delete user endpoint

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

Top comments (1)

Collapse
 
charlessoaresd2 profile image
Charles Soares

Congratulation for sharing your knowledge, I'm learning a lot with this article.
Looking forward for "Secure our endpoint"