cover

pnpm initpnpm start 跑起來,一篇搞定 Express + TypeScript 專案初始化。適合想快速起手但不想踩坑的你。

先講結論

Express + TypeScript 的專案初始化其實不難,但有幾個地方很容易卡住:tsconfig.jsonrootDir / outDir 沒設好會編譯到奇怪的地方、express-generator 生成的是 JS 檔要自己轉 TS、環境變數要記得裝 dotenv。這篇把整個流程走一遍,你照著做就能跑。

初始化專案 + 裝套件

mkdir express_ts_proto && cd express_ts_proto
pnpm init

然後一口氣把需要的東西裝起來:

# 正式依賴
pnpm add express cookie-parser morgan dotenv
 
# 開發依賴(TypeScript + 型別定義)
pnpm add -D typescript ts-node @types/node @types/express \
  @types/cookie-parser @types/morgan @types/debug

為什麼用 pnpm?因為它用 hard link 共享套件,裝過一次的東西不會重複佔空間。你如果同時開好幾個 Node 專案,node_modules 不會把硬碟吃光。npm 表示不服,但硬碟表示很服。

別忘了加 .gitignore,至少把 node_modules/dist/.env 丟進去。

設定 TypeScript

pnpm exec tsc --init

這會生出一個超長的 tsconfig.json,裡面大部分選項都是註解掉的。你真正需要改的就這幾個:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

esModuleInterop 這個一定要開。不開的話 import express from 'express' 會報錯,你得寫成 import * as express from 'express',醜到不行。

用 express-generator 生骨架,然後改成 TS

pnpm add express-generator --save-dev
pnpm exec express --no-view

這會生出一堆 .js 檔。接下來要做的事情就是:搬到 src/ 底下,副檔名改成 .ts,然後加上型別。最終的檔案結構長這樣:

/src
  /bin
    www.ts
  /routes
    index.ts
    users.ts
  app.ts
.env

核心檔案

src/app.ts — 應用的進入點,掛 middleware 和路由:

import express from 'express';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import routes from './routes';
 
const app = express();
 
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(routes);
 
export default app;

src/bin/www.ts — 啟動伺服器,處理 port 衝突等錯誤:

#!/usr/bin/env node
import 'dotenv/config';
import app from '../app';
import http from 'http';
import debug from 'debug';
 
const normalizePort = (val: string) => {
  const port = parseInt(val, 10);
  if (isNaN(port)) return val;
  if (port >= 0) return port;
  return false;
};
 
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
 
const server = http.createServer(app);
 
server.on('error', (error: NodeJS.ErrnoException) => {
  if (error.syscall !== 'listen') throw error;
  const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
    default:
      throw error;
  }
});
 
server.on('listening', () => {
  const addr = server.address();
  const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr?.port;
  debug('Listening on ' + bind);
});
 
server.listen(port);

路由就很簡單了,src/routes/index.ts 當路由的集中管理處:

import { Router, Request, Response } from 'express';
import usersRouter from './users';
 
const router = Router();
router.get('/', (req: Request, res: Response) => {
  res.send('Welcome to the Home Page');
});
router.use('/users', usersRouter);
 
export default router;

src/routes/users.ts 同理,就不重複貼了。

跑起來

.env 加上:

PORT=3000

package.json 加上 start script:

"scripts": {
  "start": "ts-node ./src/bin/www"
}

然後 pnpm start,打開瀏覽器連 http://localhost:3000,看到 “Welcome to the Home Page” 就代表搞定了。

接下來你大概會想加 ESLint、型別定義管理、還有測試 — 那些我放在 下一篇:ESLint + Typings 設定 了。


專案初始化就像搬新家:東西先搬進去能住就好,裝潢的事明天再說