Migrating RESTful API from Express.js to NestJS | MinnanoBeta

Yes Lee
8 min readAug 4, 2024

--

Why

MinnanoBeta 的 Restful API 是承襲 Paaaack 的架構用 Express.js 寫的,沒有任何的規範每個 endpoint 裡想怎麼寫就怎麼寫。

而在工作上因為有機會看過跟寫過 RoR、NestJS based API 的 code,一直都知道有更有架構、規範跟效率的 API 寫法,所以為了自己的職涯以及MinnanoBeta 的開發著想,就決定來把 Express.js migrate 成 NestJS 了!

How

みんなのBeta 的 API 中有一支 ListGyms 的 API 用來列出岩館的清單,我決定以 migrate 這支 API 的方式來當作實驗。

Before (Express.js):在 Express.js 底下寫的 API 就是這麼單純 😆

require('dotenv').config();

const { PrismaClient } = require('@prisma/client');
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');

const prisma = new PrismaClient();
const app = express();
app.use(cors());
app.use(bodyParser.json());

app.get('/climbing/gyms', async (req, res) => {
let gyms = [];
try {
gyms = await prisma.gyms.findMany({orderBy: [{ name: 'asc' }] });
} catch (err) {
console.log(err);
} finally {
res.json(gyms);
}
});

After (NestJS)

在 NestJS 裡要寫 API 的話最基本的應該是要 follow Module, Controller, Service 的架構,所以以 ListGyms API 來說,folder structure 大致如下:

src
| .env
| main.ts
| app.module.ts
| prisma
| prisma.service.ts
| gyms
| gyms.module.ts
| gyms.controller.ts
| gyms.service.ts
// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3001);
}
bootstrap();
// app.modules.ts

import { GymsModule } from '@/src/gyms/gyms.module';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [ConfigModule.forRoot(), GymsModule],
})
export class AppModule {}
// gyms.module.ts

import { Module } from '@nestjs/common';
import { GymsController } from './gyms.controller';
import { GymsService } from './gyms.service';
import { PrismaService } from '@/src/prisma/prisma.service';

@Module({
controllers: [GymsController],
providers: [GymsService, PrismaService],
})
export class GymsModule {}
// gyms.controller.ts

import { Controller, Get } from '@nestjs/common';
import { GymsService } from './gyms.service';

@Controller('climbing')
export class GymsController {
constructor(private readonly gymsService: GymsService) {}

@Get('gyms')
async listGyms() {
return this.gymsService.findAll();
}
}
// gyms.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/src/prisma/prisma.service';

@Injectable()
export class GymsService {
constructor(private prisma: PrismaService) {}
async findAll() {
try {
return await this.prisma.gyms.findMany({ orderBy: [{ name: 'asc' }] });
} catch (err) {
return [];
}
}
}
// prisma.service.ts

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}

async onModuleDestroy() {
await this.$disconnect();
}
}
  1. 在 Controller 裡定 API endpoint
  2. 在 Service 裡實作 API 的 business logic
  3. 再透過 Module 做 import / export

這是我大致上的理解。

做到這樣後就能在 local 測試 API 了。

Deployment

接著,在 deploy 的部分,Express.js 跟 NextJS 也有一點不一樣的地方是,NextJS 的 production 需要多一個 build 的步驟而 Express.js 不用,也因此相對應的就要做一點 Dockerfile 和 pm2 的 ecosystem.config.js 的設定調整。

// Dockerfile.prod

FROM node:20-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm pm2
RUN apt-get update -y && apt-get install -y openssl
RUN pnpm install --frozen-lockfile
COPY . .
RUN npx prisma generate
RUN pnpm run build
EXPOSE 3001
CMD ["pm2-runtime", "start", "ecosystem.config.js", "--only", "api-nestjs-prod"]
// ecosystem.config.js

module.exports = {
apps: [
{
name: 'api-nestjs-prod',
script: 'dist/main.js',
watch: false,
env: {
NODE_ENV: 'production',
},
},
{
name: 'api-nestjs-dev',
script: 'pnpm',
args: 'run start:dev',
watch: ['src'],
ignore_watch: ['node_modules', 'dist'],
env: {
NODE_ENV: 'development',
},
},
],
};

最後

  1. Linode 上把 NestJS based production API 跑起來
  2. 調整 Cloudflare 上的 DNS 多開一組 CNAME api-v2.{domain-name}
  3. Nginx 新增 reverse proxy 把 api-v2 endpiont 和 API 連起來

Migration 完成!

--

--