Benefits of using Typescript with ExpressJS with Project Template
Tags: typescript, node, express, javascript
Published: 2023-11-25 14:39:10

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 primary language. We will be using Docker to host MySQL on our machine. This will only work on Node Version: v20.10.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 simple blog application named bloggo.

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

npm init -y
```

To Add Typescript support
```bash
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, their functionality is:

1. **typescript:** this package is required to transpile ts to js 
2. **@types/node:** this provides type definitions like Request, Response for NodeJS
3. **ts-node:** this is combination of two package 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 detected.

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

```bash
npm i express
npm i -D @types/express
```

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

```ts
//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

```json
// 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 without rebuilding for every change done while development. 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
- **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`

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

```ts
//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

```ts
//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.

```bash
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

```ts
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

```bash
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

```bash
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
```bash
npx prisma migrate dev --name init
``` 

You can now check that tables have been connected by connecting to mysql instance. Run this commands after connecting to mysql instance
```bash
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.

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

```ts
//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.
```bash
// 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 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 few CRUD apis for Blog and User. We then added Prisma for ORM and created instance for MySQL, connected the two, ran migrations and in the end we got full functioning CRUD app.
This app is far from production like we don't have auth, request body validations, pagination, docker setup for our api and many other things. These things will be covered in later post.