DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on • Edited on

Express.js with TypeORM, PostgreSQL, and TypeScript

Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It is the most popular web framework for Node.js and for good reason. Express.js makes it easy to build web applications and APIs quickly and efficiently. This article will teach you how to build a backend REST API with Express JS, TypeORM, and PostgreSQL

Image description

Table of Content

  • Introduction
    • Technologies you will use
  • Prerequisites
    • Assumed knowledge
    • Development environment
  • Generate the Express with TypeORM and PostgreSQL project
  • Clean up the Project
  • Install Necessary packages / Setup Script
  • Configure Datasource
    • Create a PostgreSQL instance
    • Set your environment variable
    • Understand data-source configuration
    • Model the data
  • Implements CRUD operations for User model
    • Configure Service for User model(CRUD)
    • Define Repositories for User Service
    • Setup Controller for User model(CRUD)
    • Add Post Service to Controller (Dependency Injection)
    • Define GET /users endpoint
    • Define GET /users /:id endpoint
    • Define POST /users endpoint
    • Define PUT/users /:id endpoint
    • Define DELETE /users /:id endpoint
  • Summary and final remarks

Introduction

In this tutorial, you will learn how to build the backend REST API for user management. You will get started by creating a new Express project. Then you will start your own PostgreSQL server and connect to it using TypeORM. Finally, you will build the REST API and test it with Postman.

Technologies you will use

You will be using the following tools to build this application:

Prerequisites

Assumed knowledge

This is a beginner-friendly tutorial. However, this tutorial assumes:

  • Basic knowledge of JavaScript or TypeScript (preferred)
  • Basic knowledge of Express

Note: If you're not familiar with ExpressJS, don't worry you can quickly learn by following this tutorial.

Development environment

To follow along with this tutorial, you will be expected to:

  • ... have Node.js installed.
  • ... have Docker or PostgreSQL installed.
  • ... have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series. (optional)

Generate the Express Project

The first thing you will need is to install the TypeORM globally just open your terminal and run the following command.
npm install -g typeorm

You can use the TypeORM to initialize a brand-new project. To start, run the following command in the location where you want the project to reside:

typeorm init --name div --express  --database postgres --docker
Enter fullscreen mode Exit fullscreen mode

Open the project in your preferred code editor (we recommend VSCode). You should see the following files:

div
   ├── node_modules
   └── src
       ├── index.ts
       ├── routes.ts  
       └── entity
           └── User.ts
       └── controller
           └── UserController.ts
   ├── README.md
   ├── docker-compose.yml
   ├── .gitignore
   ├── tsconfig.json
   ├── package.json
   └── package-lock.json
Enter fullscreen mode Exit fullscreen mode

Important Note: Most of the code you work on will reside in the src directory. The command you run has already created a few files for you. Some of the notable ones that we need to modify to run our application are:

  • src/index.ts: This is the entry point of the application. It will start the Express application

  • src/controller/UserController.ts: This controller handles CRUD functionality.

  • src/data-source.ts: This file is responsible for configuring our database.

  • src/routes: This is the router connected to 'UserController' that creates CRUD endpoints

  • docker-compose.yml: In this file, we will set up our Docker image for our PostgreSQL database.

Clean up the Project

first, let's clean up the src/index.ts file by replacing your src/index.ts content with the following code

import * as express from "express"
import * as bodyParser from "body-parser"
import { AppDataSource } from "./data-source"
AppDataSource.initialize().then(async () => {
    const app = express()
    app.use(bodyParser.json())
    app.listen(3000)
console.log("Express server has started on port 3000")
}).catch(error => console.log(error))
Enter fullscreen mode Exit fullscreen mode

what we do is Remove unnecessary imports, route mapping, and the insertion of dummy data since we will set up more organized routing and insert our own data using endpoints later.

Second, Delete the src/routes.ts file by manually deleting it or just run the following command in your terminal:
rm src/routes.ts

Note: we deleted the routes.ts because we will make our code more organized by putting it in its own folder

to do that just run the following command in your terminal:

  • mkdir src/router - to create a router directory inside the src folder then

  • touch src/router/userRouter.ts - to create a new file named userRouter.ts inside your newly created router directory

Before we do something on the router let's first setup our PostgreSQL database to do that just replace the docker-compose.yml content with the following code

version: "3.8"
services:
  postgres:
    image: "postgres:14.5"
    ports:
      - "5440:5432"
    environment:
      POSTGRES_USER: "div"
      POSTGRES_PASSWORD: "divpassword"
      POSTGRES_DB: "divdata"

Enter fullscreen mode Exit fullscreen mode

A few things to understand about this configuration:

  • The imageoption defines what Docker image to use. Here, you are using the postgres image version 14.5.

  • The environmentoption specifies the environment variables passed to the container during initialization. You can define the configuration options and secrets – such as the username and password – the container will use here.

  • The portsoption maps ports from the host machine to the container. The format follows a 'host_port:container_port' convention. In this case, you are mapping the port 5440of the host machine to port 5432of the postgrescontainer.

Make sure that nothing is running on port 5440of your machine. To start the postgrescontainer, open a new terminal window and run the following command in the main folder of your project:
docker-compose up -d

If everything works correctly, you will see on your terminal something like this

[+] Running 3/3
 ✔ Network div_default       Created                                                                                             0.8s 
 ✔ Volume "div_postgres"     Created                                                                                             0.0s 
 ✔ Container div-postgres-1  Started     
Enter fullscreen mode Exit fullscreen mode

Note: we added a -d flag at the end of the docker-compose up. This will run the container in the background.

Congratulations 🎉. You now have your own PostgreSQL database to play around with!


> Note: If you prefer not to use Docker, you can set up a local PostgreSQL instance on your machine using PgAdmin. Just make sure to provide the correct value in our .env file

Set your environment variable

we will now create our .env file inside of our root directory to do this run the following command on your terminal
touch .env
now open the .env file that we created and populate with the following code

POSTGRES_DB=divdata
POSTGRES_PORT=5440
POSTGRES_HOST=localhost
POSTGRES_USERNAME=div
POSTGRES_PASSWORD=divpassword
POSTGRES_SYNC=true
Enter fullscreen mode Exit fullscreen mode

You might be wondering where we obtain this data. Well, these values correspond to the configurations in our docker-compose.yml file. If you revisit that file, you will see that we have set up the database, username, password, and *port * number there later I'll explain the user of *POSTGRES_SYNC * when we configure the data-source file for now just paste this code on your .env file

Note: the .env file enables you to manage sensitive information consistently while maintaining its security that's why we use it to store all our sensitive information including our database password and username.

Important Note: in order for us to use the values that we defined inside the .env file we will need to install a third-party library called dotenv you can install this by running the following command on your terminal npm install dotenv

Install additional packages/ Setup Script

We are going to install three important packages, and then I'll explain their usage. First, let's install the 'nodemon', concurrently and 'rimraf' packages. We can do this by running the following code in our terminal:
npm install nodemon rimraf concurrently

Nodemon:
Nodemon is a tool that automatically restarts your Node.js server when you make code changes. It's great for speeding up development.

Rimraf:
Rimraf is a utility to quickly and safely delete directories and their contents, useful for cleaning up files and folders in Node.js projects.

Concurrently
Concurrently is a Node.js package that lets you run multiple commands at the same time in a single terminal, simplifying complex development tasks.

Setup Script

inside your package.json files update the scripts with the following code:

  "scripts": {
    "dev": "nodemon",
    "build": "rimraf build && concurrently \"tsc\"",
    "start": "npm run build && nodemon build/index.ts",
    "typeorm": "typeorm-ts-node-commonjs"
  }
Enter fullscreen mode Exit fullscreen mode

Note: This script configuration includes:
"dev" : Runs Nodemon for development.
"build" : Deletes the "build" directory, compiles TypeScript using "tsc," and uses the "concurrently" package.
"start" : Builds the project, then runs Nodemon on the "build/index.ts" file.
"typeorm" : Runs the "typeorm-ts-node-commonjs" command.

Now let's create nodemon.json files inside the root of our directory we do that using the following command in your terminal
touch nodemon.json
then populate the nodemon.json with the following code

{
  "watch": ["src"],
  "ignore": ["src/**/*.test.ts", "node_modules"],
  "ext": "ts,mjs,js,json,graphql",
  "exec": "npm run build && node ./build/index.js",
  "legacyWatch": true
}

Enter fullscreen mode Exit fullscreen mode

Note: This configuration watches files in the "src" directory,
and executes "npm run build" and "node ./build/index.js" when changes are detected.
"legacyWatch" is set to true for compatibility.


You might be wondering about the reference to "./build/index.js" in the "start" script, especially when we're working with TypeScript. The "index.js" we're referring to is the compiled output of our TypeScript code. When you transpile TypeScript into JavaScript, the resulting JavaScript file typically has an "index.js" name by default. This is the entry point for your application and is located in the "build" directory, as specified by the path "./build/index.js."

We know that the compiled folder will be named "build" because it is configured in the tsconfig.json file (try opening this file and see for yourself what I'm talking about). This configuration file for TypeScript allows you to specify various settings, including the output directory for your compiled code. In this case, the "build" folder is set as the output directory in your tsconfig.json file, ensuring that the transpiled JavaScript files are placed there. So, when we reference "./build/index.js," we're pointing to the output directory where our TypeScript code has been compiled into JavaScript.


Configure the data-source

Open your data-source.ts file and replace the current content with the following code:

import "reflect-metadata";
import { DataSource } from "typeorm";
import "dotenv/config";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: process.env.POSTGRES_HOST,
  port: Number(process.env.POSTGRES_PORT),
  username: process.env.POSTGRES_USERNAME,
  password: process.env.POSTGRES_PASSWORD,
  database: process.env.POSTGRES_DB,
  synchronize: !!process.env.POSTGRES_SYNC,
  logging: !!process.env.POSTGRES_LOGGING,
  entities: ["build/entity/*.js", "build/entity/**/*.js"],
  migrations: ["build/migrations/*.js"],
  subscribers: ["build/subscriber/**/*.js"],
  ssl: !!process.env.POSTGRES_SSL,
});

Enter fullscreen mode Exit fullscreen mode

process.env is used to reference the variable that we defined inside .env file

Notable properties include:

  • synchronize : It synchronizes the database schema with your entities if set to true. It automatically updates your database when you change something on your entity without having to run the migration command.

  • logging : If set to true, it enables database query logging.

  • entities : Lists the JavaScript files that define the entities used in the database. These entities describe the database tables and their relationships.

  • migrations : Lists JavaScript files that contain database migration scripts.

  • subscribers : Lists JavaScript files that contain database event subscribers.

  • ssl : It's enabled if process.env.POSTGRES_SSL is truthy, which typically sets up a secure connection to the PostgreSQL database.

run npm run dev if you sell the logs
Express server has started on port 3000.

Congratulations 🎉. Your PostgreSQL database and express app are now connected successfully!

Before we get carried away, let's work on our entity and then set up CRUD functionality to ensure that our database is truly connected to our application.

since we already have the entity and controller folder
Let's also create the service and repository folders inside our src directory

  1. service: In this folder, all the business logic in our application will reside.

  2. repository: In this folder, all the connections between our entity and our service will be established.

  3. controller: In this folder, all the data accepted from users or clients will be received and passed to the service (e.g., request.body, req.params...)

  4. Entity: This is where all our models related to the database tables and database relationships will reside.

to do that just run the following command in your terminal:

  • mkdir src/service- to create a service directory inside the src folder then

  • touch src/service/userService.ts - to create a new file named userService.ts inside your newly created service directory

  • mkdir src/repository - to create a repository directory inside the src folder then

  • touch src/repository/index.ts - to create a new file named index.ts inside your newly created repository directory

if you follow along with all the instruction above your file structure should now look like this

div
   ├── build
   ├── node_modules
   └── src
       └── controller
           └── UserController.ts
       └── entity
           └── User.ts
       └── migration
       └── repository
           └── index.ts
       └── router
           └── userRouter.ts
       └── service
           └── userService.ts
       ├── index.ts
       └── data-source.ts
   ├── .env
   ├── .gitignore
   ├── docker-compose.yml
   ├── nodemon.json
   ├── package-lock.json
   ├── package.json
   ├── README.md
   └── tsconfig.json

Enter fullscreen mode Exit fullscreen mode

Now that we've set up all the necessary files for creating our CRUD functionality, let's get our hands dirty and start working on our files. We'll begin with the "userService.ts" file.

Open your userService.ts and populate it with the following content

//userService.ts
import { Repository } from "typeorm";
import { User } from "../entity/User";

export class UserService {
  constructor(private readonly userRepository: Repository<User>) {}
  async findAll() {
    const users = await this.userRepository.find();
    return users;
  }
  async findOne(id: number) {
    const users = await this.userRepository.findOne({ where: { id } });
    return users;
  }

  async createUser(newuser: User) {
    const user = this.userRepository.create(newuser);
    await this.userRepository.save(user);
    return user;
  }

  async updateUser(id: number, data: Partial<User>) {
    const user = await this.userRepository.findOne({ where: { id } });
    if (user) {
      this.userRepository.merge(user, data);
      await this.userRepository.save(user);
      return user;
    } else {
      return { message: "User not found" };
    }
  }

  async delete(id: number) {
    const user = await this.userRepository.findOne({ where: { id } });

    if (user) {
      await this.userRepository.remove(user);
      return { message: "User Deleted successfully" };
    } else {
      return { message: "User not found" };
    }
  }

}

Enter fullscreen mode Exit fullscreen mode

Brief Code explanation (userService)

-The constructor in the "userService.ts" file is used to inject a dependency into the UserService class. In this case, it injects a Repository instance, which is used to interact with the database for operations related to the User entity.

  • find(): This method is used to retrieve multiple records from the database that match certain criteria. In this case, await this.userRepository.find() is used to fetch all the users from the database.
  • findOne(): It retrieves a single record from the database based on specified criteria. await this.userRepository.findOne({ where: { id } }) is used to find a user by their id.

  • create(): This method is used to create a new instance of the entity (in this case, a User entity) without saving it to the database. this.userRepository.create(newuser) creates a new user instance without persisting it to the database.

  • save(): After creating an entity using the create() method or making changes to an existing entity, the save() method is used to persist the changes to the database. await this.userRepository.save(user) saves the newly created user to the database, or it saves any changes made to an existing user entity.

  • remove(): This method is used to delete a record from the database. await this.userRepository.remove(user) is used to delete a user from the database.


Let now proceed to the repository/index.ts file
Open it and populate it with the following content

//repository/index.ts
import { AppDataSource } from "../data-source";
import { User } from "../entity/User";
import { UserService } from "../service/userService";

export const userRepository = new UserService(
  AppDataSource.getRepository(User)
);

Enter fullscreen mode Exit fullscreen mode

Brief Code explanation (repository/index.ts)

Creating a Repository: The key part of this code is the creation of a userRepository. Here's how it works:

  • AppDataSource.getRepository(User) is called. This suggests that AppDataSource has a method called getRepository() that is used to get a repository for the User entity. In TypeORM, repositories are used to perform database operations on entities.

  • The retrieved repository is then passed as a parameter to the UserService constructor, creating a new instance of the UserService. this repository will be used within the UserService to perform database operations related to users.

Note: this is a common pattern in applications using TypeORM to organize database-related logic.


Let now proceed to the controller/UserController.ts file
Open it and populate it with the following content

//controller/userController.ts
import { Request, Response } from "express";
import { userRepository } from "../repository";

export class UserController {
  static async all(request: Request, response: Response) {
    const data = await userRepository.findAll();
    return response.status(200).send(data);
  }

  static async create(request: Request, response: Response) {
    const data = await userRepository.createUser(request.body);
    return response.status(201).send(data);
  }

  static async findOne(request: Request, response: Response) {
    const id = Number(request.params.id);
    const data = await userRepository.findOne(id);
    return response.send(data);
  }

  static async update(request: Request, response: Response) {
    const id = Number(request.params.id);
    const data = await userRepository.updateUser(id, request.body);
    return response.send(data);
  }

  static async delete(request: Request, response: Response) {
    const id = Number(request.params.id);
    const data = await userRepository.delete(id);
    return response.send(data);
  }
}

Enter fullscreen mode Exit fullscreen mode

Brief Code explanation (controller/UserController.ts)

Controller Class: The UserController class is defined as a static class, which means you don't need to create an instance of this class to use its methods. It's meant to provide a set of static methods for handling various HTTP routes.

all(request, response): This method is for handling a route where you want to retrieve all users. It calls the userRepository.findAll() method to fetch all users from the database and then sends the data in the response.

create(request, response): This method is for handling a route to create a new user. It calls userRepository.createUser(request.body) to create a new user based on the data in the request body and sends a 201 (Created) response with the created user.

findOne(request, response): This method handles a route to find a specific user by their ID. It extracts the id from the request parameters, calls userRepository.findOne(id) to find the user, and sends the user data in the response.

update(request, response): This method handles a route to update a user's information. It extracts the id from the request parameters, and calls userRepository.updateUser(id, request.body) to update the user's data, and send the updated data in the response.

delete(request, response): This method handles a route to delete a user. It extracts the id from the request parameters, calls userRepository.delete(id) to delete the user, and sends a response indicating the success or failure of the operation.

Note: The methods findAll(), createUser(), findOne(), updateUser(), and delete() are defined within your userService class, and they are responsible for interacting with the repository to perform CRUD operations on users.


Let now proceed to the router/UserRouter.ts file
Open it and populate it with the following content

import { Router } from "express";
import { UserController } from "../controller/UserController";

const userRouter = Router();
userRouter.get("/users", UserController.all);
userRouter.post("/users", UserController.create);
userRouter.get("/users/:id", UserController.findOne);
userRouter.put("/users/:id", UserController.update);
userRouter.delete("/users/:id", UserController.delete);

export default userRouter;

Enter fullscreen mode Exit fullscreen mode

Brief Code explanation (router/userRouter.ts)

Route Definitions:
A series of HTTP routes are defined on the userRouter using methods like get, post, put, and delete. Each route corresponds to a specific user-related operation.

userRouter.get("/users", UserController.all); defines a route to handle HTTP GET requests at the "/users" endpoint. It delegates the request handling to the UserController.all method, which is responsible for fetching all users.

userRouter.post("/users", UserController.create); defines a route to handle HTTP POST requests at the "/users" endpoint. It delegates the request handling to the UserController.create method, which is responsible for creating a new user.

userRouter.get("/users/:id", UserController.findOne); defines a route to handle HTTP GET requests at the "/users/:id" endpoint. The ":id" part is a route parameter that allows for dynamic ID values. It delegates the request handling to the UserController.findOne method, which is responsible for finding a user by their ID.

userRouter.put("/users/:id", UserController.update); defines a route to handle HTTP PUT requests at the "/users/:id" endpoint. It also uses the ":id" route parameter to identify the user to be updated. It delegates the request handling to the UserController.update method, which updates a user's information.

userRouter.delete("/users/:id", UserController.delete); defines a route to handle HTTP DELETE requests at the "/users/:id" endpoint, where ":id" is a dynamic parameter for identifying the user to be deleted. It delegates the request handling to the UserController.delete method, which deletes a user.

Route Export:
export default userRouter; exports the userRouter object so that it can be used in other parts of your application to define the actual API routes.


and for the final tasks before we test our endpoints let's call in inside our src/index.ts to define the routing and connect it to our main Express application (app). define it like this app.use("/api", userRouter); your src/index.ts should look like this now

import * as express from "express";
import * as bodyParser from "body-parser";
import { AppDataSource } from "./data-source";
import userRouter from "./router/userRouter";

AppDataSource.initialize()
  .then(async () => {
    const app = express();
    app.use(bodyParser.json());
    app.use("/api", userRouter);
    app.listen(3000);

    console.log("Express server has started on port 3000.");
  })
  .catch((error) => console.log(error));

Enter fullscreen mode Exit fullscreen mode

Now, run npm run dev in your terminal. If you see a log message saying, 'Express server has started on port 3000,' then we're good to go for testing our application using Postman or Thunder client.


Before we proceed with testing our application, it's important to understand that in the following code snippet app.use("/api", userRouter);, the '/api' prefix signifies that all middleware or routes defined in the userRouter will be applied to any route that starts with '/api' in the URL.

For example, within our userRouter, there's a route defined like this: userRouter.get("/users", UserController.all);. Given that our application is running locally on port 3000, the complete endpoint for this route would be 'http://localhost:3000/api/users'. This means that the '/api' prefix is added to all routes defined in the userRouter, and you can access them by starting with '/api' followed by the specific route path.

*here is the list of all our user routes with /api as prefix *
POST: http://localhost:3000/api/users
GET: http://localhost:3000/api/users
GET: http://localhost:3000/api/users/:id
PUT: http://localhost:3000/api/users/:id
DELETE: http://localhost:3000/api/users/:id


TEST OUR APPLICATION

Create User
on our entity, we declared three columns (firstName, lastName, age)

Image description

Get all Users

Image description

Get Single User

Image description

update User

Image description

Delete User

Image description


Summary and Final Remarks

Congratulations! You've successfully developed a basic REST API using the Express.js framework, combined with TypeORM and TypeScript. In this tutorial, you achieved the following milestones:

Built a REST API with Express.js and TypeScript: You created a RESTful API using Express.js, a popular Node.js framework, and TypeScript, a statically-typed superset of JavaScript. TypeScript provides strong type checking and enhances code quality and readability.

Seamlessly Integrated TypeORM in an Express.js Project: You smoothly integrated TypeORM, a powerful Object-Relational Mapping (ORM) library, into your Express.js project. This integration allows for efficient database interactions and data modeling while leveraging TypeScript's type safety.

Modularized Your Code: Your application's codebase is modular, allowing for a clean separation of concerns. You've structured your code into modules, such as routers and controllers, making it easier to manage, test, and maintain.

Test Your Application Using Postman: You are now ready to test your application using Postman or a similar tool to verify that your API endpoints work as expected and interact with your API.

One of the key takeaways from this tutorial is the synergy between TypeScript and Express.js. This combination enhances code quality, provides type safety, and promotes modularity, making it an ideal choice for developing robust and scalable backend APIs for a wide range of applications.

Code for this Lesson on Github

Top comments (1)

Collapse
 
dotenv profile image
Dotenv

💛🌴