Benefits of using Typescript with ExpressJS with Project Template
Sumit Bhanushali
Posted on November 25, 2023
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.
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, their functionality is:
- typescript: this package is required to transpile ts to js
- @types/node: this provides type definitions like Request, Response for NodeJS
- 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
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 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⌚ 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 connected 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 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.
Posted on November 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 24, 2024