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
First, make a folder, you can name it whatever you want. I will name mine express-ts-api.
mkdir express-ts-api
After that initialize a node project.
npm init --y
We also need to install TypeScript.
npm i -D typescript
We also need to install type definitions for these Express and Node.
npm i -D @types/express @types/node
And also, we will install Express
npm i express
Lastly, configure this project to be a TypeScript project.
Using this command
tsc -init
Our tsconfig.json will look like this.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
}
}
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';
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"
}
}
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
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
}
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) => {
})
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
})
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
})
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
})
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
})
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
})
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.
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
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
})
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.
Top comments (7)
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.
I will try this in my next project.
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.
Try Hapi as well. It is pretty nice too.
It was a very explanation. Looking forward for some content.
Thanks for this post...
why do you use ES5 for a modern node application?
e.g. the reference base config for node14 uses ES2020