How to create a node.js api server using typescript
Published: 2023-08-29 12:57:26

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
```javascript 
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
```javascript
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.
```javascript
{
    "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. 
```javascript
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.

```javascript
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
```javascript
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.
```javascript

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;

```javascript
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.
```javascript
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.
```javascript
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;
```javascript
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;
```javascript
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;
```javascript
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.
```javascript
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;
```javascript
 "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

![Get all users endpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/opauz6uq879nbojk2na5.png)

**2. Create user**

![Create user endpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/015dla17zf58ycj09w53.png)

**3. Update user**

![Update user endpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fm444qliu23ruzm723j0.png)
**4. Delete user**

![Delete user endpoint](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9pgfyra5s9yodkfq6fr.png)

## 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](https://github.com/DallingtonAsin/nodejs-api-server.git)