DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Get Better with TypeScript using Express
Mark Abeto
Mark Abeto

Posted on

Get Better with TypeScript using Express

Hi Guys Good Day!

Today, we will make a Rest API using these two technologies. You may think that this is another Rest API like all the others but in this example, we will be using a lot of TypeScript advanced features that will be really helpful for this demo. But we will be more focused on using TypeScript instead of the business logic implementation. I suggest using VSCode in this example because it provides a lot of features with TypeScript.

The API we will be making will focus on Dogs. Our endpoints will look like this.

GET /api/v1/dogs
GET /api/v1/dogs/:id
POST /api/v1/dogs
PUT /api/v1/dogs/:id
DELETE /api/v1/dogs/:id
Enter fullscreen mode Exit fullscreen mode

dog typing

First, make a folder, you can name it whatever you want. I will name mine express-ts-api.

  mkdir express-ts-api
Enter fullscreen mode Exit fullscreen mode

After that initialize a node project.

 npm init --y
Enter fullscreen mode Exit fullscreen mode

We also need to install TypeScript.

 npm i -D typescript
Enter fullscreen mode Exit fullscreen mode

We also need to install type definitions for these Express and Node.

 npm i -D @types/express @types/node
Enter fullscreen mode Exit fullscreen mode

And also, we will install Express

 npm i express
Enter fullscreen mode Exit fullscreen mode

Lastly, configure this project to be a TypeScript project.
Using this command

  tsc -init
Enter fullscreen mode Exit fullscreen mode

Our tsconfig.json will look like this.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Put simply, this config tells us that our code will be outputted on es5 syntax ("target": "es5") and also that code will be using the CommonJS module system ("module": "commonjs") in the directory build ("outDir": "./build") base on the contents in the src directory ("rootDir": "./src") and the typescript language service should enforce strong type checking ("strict": "true") and lastly we want to import modules in different Module Systems like commonjs follow the specifications of the ES6 module ("esModuleInterop": true) without this option our imports will look like this

import * as express from 'express';
// instead of this
// import express from 'express';
Enter fullscreen mode Exit fullscreen mode

Our package.json will look like this.

{
  "name": "express-ts-api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.4",
    "typescript": "^4.4.4"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

I will assume that you already know nodejs, so I will not explain the contents of this file.

Ok, let's get started. Make the src directory

 mkdir src
Enter fullscreen mode Exit fullscreen mode

Inside the src folder make the app.ts file.

import express from 'express'

const app = express()

interface Dog {
  name: string
  breed: 'labrador' | 'german shepherd' | 'golden retriever'
  adopted_at: Date | null
  birth_date: Date | null
}
Enter fullscreen mode Exit fullscreen mode

Btw, every HTTP method that we use in the app object has five generic types that we can provide our custom types. The arrangement of the types are Params, ResBody, ReqBody, ReqQuery and Locals. More in Generics here. Basically, Generics helps us reuse code but in our case, we can reusable types.

app.get<Params,ResBody,ReqBody,ReqQuery,Locals>('/api/v1/dogs',
(req,res) => { 

})
Enter fullscreen mode Exit fullscreen mode

We will only be using the first four generic types in this example. By default the Params generic type have a value of a type of an empty object. ResBody and ReqBody has a type of any, and lastly the ReqQuery has a type of ParsedQs.

We will be providing our own types instead of the default types provided by express.

GET /api/v1/dogs

app.get<
{},
{ data: Dog[], message: string },
{},
{ page: number, limit: number, breed: 'labrador' | 'german shepherd' | 'golden retriever' }>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we will be getting a list of dogs so we will not pass a type in the Params generic because we are not getting a specific dog. We are getting a list of dogs in this endpoint, so we will leave it as an empty object. Second, the ResBody is the type that we will send in res.send method, in this case, we will send an object that has two properties data whose type is an array of Dogs that we provided earlier and message whose type is a string an additional information for the response. Third, the ReqBody is an empty object type because we won't be receiving any data in this endpoint. And lastly, in the ReqQuery we will be passing an object type that accepts the pagination properties page and limit and also can the breed property so we can use it to filter dogs base on a specific breed.

GET /api/v1/dogs/:id

app.get<
{ id: number },
{ data: Dog | null, message: string },
{}>
('/api/v1/dogs/:id', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we will be getting a specific dog so we will pass an object type in the Params whose property is an id which has the type of number because we will be getting a specific dog. We are getting a list of dogs in this endpoint, so we will leave it as an empty object. Second, the ResBody in this case, we will send an object that has two properties data whose type are a union type of the Dog type and null this tells us that if the dog exists it will return the shape of the Dog and if does not exist it will return null and the property message whose type is a string. Third, the ReqBody is also an empty object type because we won't be receiving any data in this endpoint. And lastly, we will be passing an empty object type for the ReqQuery because this endpoint does not need it.

POST /api/v1/dogs

app.post<
{},
{ data: Dog & { id: number }, message: string },
Dog,
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we will be creating a new dog so we will pass an empty object type in the Params. Second, the ResBody, in this case, we will send an object that has two properties data whose type is a union type of the Dog type and an object type which has a property of id which is type number because the DB will generate this id instead of the client and the property message whose type is a string. Third, the ReqBody has a type of Dog because we will be receiving data from the client which has the shape of Dog. And lastly, we will be passing an empty object type for the ReqQuery because this endpoint does not need it.

PUT /api/v1/dogs/:id

app.put<
{ id: number },
{ data: Dog & { id: number }, message: string },
Partial<Dog>,
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we will be update an existing dog so we will pass an object type in the Params whose property is an id which has the type of number. Second, the ResBody, in this case, we will send an object that has two properties data whose type is a union type of the Dog type and an object type which has a property of id which is type number because we will return the updated value of the resource and also the property message whose type is a string. Third, the ReqBody has a type of Dog because we will be receiving data from the client which has the shape of Dog but every property should be optional because this is an update so we are using a Utility Type Partial that makes every property in the Dog interface optional. And lastly, we will be passing an empty object type for the ReqQuery because this endpoint does not need it.

DELETE /api/v1/dogs/:id

app.delete<
{ id: number },
{ data: Dog & { id: number }, message: string },
{},
{}>
('/api/v1/dogs', (req,res) => { 
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we will be deleting a dog so we will pass an object type in the Params whose property is an id which has the type of number. Second, the ResBody, in this case, we will send an object that has two properties data whose type is a union type of the Dog type and an object type which has a property of id which is type number because we will return the deleted dog resource and also the property message whose type is a string. Third, the ReqBody is an empty object type because we won't be receiving any data in this endpoint. And lastly, we will be passing an empty object type for the ReqQuery because this endpoint does not need it.

I think we're done.

meme done

I think we're not done yet. We've been passing our own custom types directly and some of those types were repeating in some of our methods and this makes our code not clean. Let's change that.

interface BaseParams<IDType = number> {
  id: IDType
}

interface DogDetails {
  name: string
  breed: DogBreed
  adopted_at: Date | null
  birth_date: Date | null
}

interface APIResponse<Data> {
  data: Data
  message: string
}

interface Pagination {
  page: number
  limit: number
  breed: DogBreed
}

interface Empty {

}

type DogBreed = 'labrador' | 'german shepherd' | 'golden retriever'

type Dog = BaseParams & DogDetails
Enter fullscreen mode Exit fullscreen mode

Ok, I will explain all these new types that you see. First, the interface BaseParams is the type we will provide to the Params position, the BaseParams has Generic type IDType which has a default value of type number you can also provide a different type for the id by passing another type here BaseParams<string> . The interface DogDetails is the type we will use for the ReqBody position. The interface APIResponse is the type that we will use for the ResBody position, this type also has a generic just like the type BaseParams, the generic ResultType type will be the type of the data property. The interface Pagination is the type we will use for the position ReqQuery, this type has a property breed the references another custom type that we will be talking about soon. The interface Empty is a helper interface type that we will use for empty objects. The DogBreed type alias is also a helper type that referenced in the Pagination interface and also the DogDetails interface. And, lastly, the Dog type alias is the combination of two interfaces BaseParams and DogDetails we achieved this by using the & intersection type.

If we apply all these new types in our code, our code should look like this.

import express from 'express'

const app = express()

interface BaseParams<IDType = number> {
  id: IDType
}

interface DogDetails {
  name: string
  breed: DogBreed
  adopted_at: Date | null
  birth_date: Date | null
}

interface APIResponse<Data> {
  data: Data
  message: string
}

interface Pagination {
  page: number
  limit: number
  breed: DogBreed
}

interface Empty {

}

type DogBreed = 'labrador' | 'german shepherd' | 'golden retriever'

type Dog = BaseParams & DogDetails

app.get<Empty, APIResponse<Dog[]>, Empty, Pagination>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.get<BaseParams, APIResponse<Dog | null>, Empty, Empty>('/api/v1/dogs/:id', (req, res) => {
  // your implementation
})

app.post<Empty, APIResponse<Dog>, DogDetails, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.put<BaseParams, APIResponse<Dog>, Partial<DogDetails>, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})

app.delete<BaseParams, APIResponse<Dog>, Empty, Empty>('/api/v1/dogs', (req, res) => {
  // your implementation
})
Enter fullscreen mode Exit fullscreen mode

This new code is more readable and more maintainable than the old one because of the new types that we made.
I think we're really done here.

labrador-done

Thank you guys for reading this post.

Have a Nice Day πŸ˜ƒ!.

Top comments (8)

Collapse
 
namhle profile image
Nam Hoang Le

The typing for Params part in POST handler should be never , so we can avoid accidental pick unwanted thing later. It will also pass the typescript warning about object.
The Query part could be omitted too.

Collapse
 
yashdesai profile image
Yash Desai

I will try this in my next project.

Collapse
 
insidewhy profile image
insidewhy

There are much better APIs for building servers now than express. Try koa, it's made by the same team as express but using modern techniques like promises.

Collapse
 
linhtch90 profile image
Linh Truong Cong Hong

Try Hapi as well. It is pretty nice too.

Collapse
 
yashdesai profile image
Yash Desai

It was a very explanation. Looking forward for some content.

Collapse
 
drsimplegraffiti profile image
Abayomi Ogunnusi

Thanks for this post...

Collapse
 
cabbage profile image
cab

why do you use ES5 for a modern node application?

e.g. the reference base config for node14 uses ES2020

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.