Benefits of using Typescript with ExpressJS with Project Template

Benefits of using Typescript with ExpressJS with Project Template

Less Bugs, Code Fast without Fear

In this post, we will setup our Backend service. We will be using ExpressJS for APIs, MySQL for storage and Prisma for ORM. We will be using Typescript as the primary language. We will be using Docker to host MySQL on our machine. This will only work on Node Version: v20.6.0 and above.

Why Typescript?

TypeScript enhances Express.js web development by introducing static typing, catching potential bugs early and improving code maintainability. With superior IDE support and modern JavaScript features, TypeScript offers a productive development experience.TypeScript with Express.js combines the simplicity of Express.js with TypeScript's static typing benefits, resulting in more reliable and maintainable web applications.

Our project will be a simple blog application named Bloggo.

mkdir bloggo && cd bloggo && touch README.md && echo -e "# Bloggo \n\nSimple Blog Application" > README.md

npm init -y

To Add Typescript support

npm i -D typescript @types/node ts-node

npx tsc --init --rootDir ./ --outDir .dist --target es2020 --allowJs --noImplicitAny false

npm i -g nodemon

Here we have installed 3 packages as dev dependencies:

  1. typescript: this package is required to transpile ts to js

  2. @types/node: this provides type definitions like Request and Response for NodeJS

  3. ts-node: this is a combination of two packages ts transpiles given ts files to js files and node executes those js files

we use tsc package to generate tsconfig.json for us. We need tsconfig.json file to instruct how to handle our ts files

  • --rootDir: location to all ts files

  • --outDir: location to place transpiled js files

  • --target: javascript version for js files to be generated

  • --allowJs: whether it should also allow js files, if disabled it will only allow ts files

  • --noImplicitAny: if true, will not allow any types to have ambigious 'any' declaration

nodemon will be used to watch our files and refresh automatically when any changes are detected.

Next, we will install express and @types/express for building APIs

npm i express
npm i -D @types/express

Let's create a basic server which will just respond with Hello World on port 3000

//index.ts
import express, { Request, Response } from 'express';

const app = express();
const port = 3000;

app.get('/', (req: Request, res: Response) => {
    res.send("hello world");
})

app.listen(port, () => {
    console.log(`listening on ${port}`);
})

We will run this using nodemon we had previously installed, we will add some npm scripts in package.json to simplify our efforts

// code elided

"scripts": {
    "start:dev": "nodemon --exec node -r ts-node/register --env-file=config.env index.ts",
    "build": "tsc",
    "build:watch": "tsc --watch"
}

// code elided
  • start:dev: we need to use ts-node we had installed previously to run ts files. Since ts-node does not directly support --env-file yet, we are using -r to require (import) ts-node/register

  • build: tsc which is part of typescript package will transpile our ts into js and place it in .dist folder as mentioned in tsconfig.json file. This will be used when we want to deploy to production

  • build:watch: This will run build in watch mode

On running npm run start:dev on terminal, our app will start running on port 3000. When you will navigate to localhost:3000 you will receive hello world.

Next, we will add some routes for interacting with blog and user resource. Create a folder named routes and add two files named blog.router.ts and user.router.ts

// blog.router.ts
import express, { Request, Response } from 'express';
const router = express.Router()

router.get('/', (req: Request, res: Response) => {
    res.send('List of blogs');
});

router.get('/:id', (req: Request, res: Response) => {
    res.send(`Blog: ${req.params.id} `)
});

router.post('/', (req: Request, res: Response) => {
    res.send(`Blog created`);
});

router.delete('/:id', (req: Request, res: Response) => {
    res.send(`Blog with id: ${req.params.id} deleted`);
});

export default router
//user.router.ts
import express, { Request, Response } from 'express';
const router = express.Router()

router.get('/', (req: Request, res: Response) => {
    res.send('List of users');
});

router.get('/:id', (req: Request, res: Response) => {
    res.send(`User: ${req.params.id} `)
});

router.post('/', (req: Request, res: Response) => {
    res.send(`User created`);
});

router.delete('/:id', (req: Request, res: Response) => {
    res.send(`User with id: ${req.params.id} deleted`);
});

export default router

Import these routes in our main index.ts file

//index.ts

import blogRouter from './routes/blog.route';
import userRouter from './routes/user.route';

app.use('/blog', blogRouter);
app.use('/user', userRouter);

You can now access respective routes at /blog/ and /user/

Next We need to add Database for storing our data. We will use image of MySQL from docker hub and prisma for ORM for performing CRUD operations on MySQL. We will start with prisma.

npm install prisma --save-dev
npx prisma init --datasource-provider mysql

This will generate prisma folder in our root directory where in schema.prisma will contain database connection details and prisma models which will be used to create/update tables in our database. We will add two models in schema.prisma

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  blogs     Blog[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Blog {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

This model files are self-explanatory. You can check more about prisma model here: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference Next step is to execute prisma script to run migration which will create these model files inside our database. But before this we need to setup our MySQL instance

docker pull mysql

docker run -d --name mysqldb -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password -d mysql

This will pull mysql image from docker hub and create container from it, we have set container name as mysqldb and root password as password. Our db will listen on port 3306

docker exec -it mysqldb sh

mysql -u root -p 
// enter "password" when prompted

create database bloggo;

This way we can attach to our db container and execute our bash command to connect to mysql and create database named "bloggo". type exit to exit from mysql and container respectively. We will update our environment variable to our credentials in config.env file

DATABASE_URL="mysql://root:password@localhost:3306/bloggo"

Next we will execute prisma script to run migrations, this command also generates prisma client files which comes in handy when using ORM

npx prisma migrate dev --name init

You can now check that tables have been created by connecting to MySQL instance. Run this commands after connecting to mysql instance

use bloggo;
show tables;

describe Blog;
describe User;

Now we can use ORM to perform CRUD operations on our database. Let's update our route files to use prisma client to run operations.

// user.route.ts

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

router.get('/', async (req: Request, res: Response) => {
    const users = await prisma.user.findMany();
    res.send(users);
});

router.get('/:id', async (req: Request, res: Response) => {
    const user = await prisma.user.findUnique({
        where: {
            id: parseInt(req.params.id),
        },
    })

    res.send(user);
});

router.post('/', async (req: Request, res: Response) => {
    const user = await prisma.user.create({
        data: req.body
    })

    res.send(user);
});

router.delete('/:id', async (req: Request, res: Response) => {
    await prisma.user.delete({
        where: {
            id: parseInt(req.params.id),
        },
    })
    res.send(`User with id: ${req.params.id} deleted`);
});
//blog.route.ts

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

router.get('/', async (req: Request, res: Response) => {
    const blogs = await prisma.blog.findMany();
    res.send(blogs);
});

router.get('/:id', async (req: Request, res: Response) => {
    const blog = await prisma.blog.findUnique({
        where: {
            id: parseInt(req.params.id),
        },
    })

    res.send(blog);
});

router.post('/', async (req: Request, res: Response) => {
    const createdBlog = await prisma.blog.create({ data: req.body });
    res.send(createdBlog);
});

router.delete('/:id', async (req: Request, res: Response) => {
    await prisma.blog.delete({
        where: {
            id: parseInt(req.params.id),
        },
    })
    res.send(`Blog with id: ${req.params.id} deleted`);
});

Now, we can create user and their blog using your favorite tool like Postman.

// First we create a dummy user

curl --location 'localhost:3000/user' \
--header 'Content-Type: application/json' \
--data '{
    "email": "abc@test.com"
}'

// Then blog from userId returned from previous command. In this case, 
authorId is 1

curl --location 'localhost:3000/blog' \
--header 'Content-Type: application/json' \
--data '{
    "title": "Title",
    "content": "Content",
    "authorId": 1
}'

This finishes up our setup of CRUD app. We started by first creating an empty directory for our blog application and initializing it with npm to setup package.json.

We added Typescript support by installing few packages and generating tsconfig.json.

We added a few CRUD apis for Blog and User. We then added Prisma for ORM and created an instance for MySQL, connected the two, ran migrations and in the end we got a fully functioning CRUD app.

This app is far from production like we don't have auth, request body validations, pagination, docker setup for our API server and many other things. These things will be covered in the upcoming posts.