- สร้าง REST API ด้วย Express และ TypeScript แบบเป็นระบบ
- 1. สร้างโปรเจกต์
- 2. ตั้งค่า package.json
- 3. ตั้งค่า TypeScript
- 4. โครงสร้างโปรเจกต์
- 5. สร้าง Type สำหรับ Product
- 6. สร้าง Schema สำหรับ Validation
- 7. สร้าง Controller
- 8. สร้าง Router
- 9. สร้าง 404 และ Error Handling Middleware
- 10. สร้าง Express Application
- 11. สร้าง Server
- 12. สร้าง .gitignore
- 13. รัน API
- 14. ทดสอบ REST API
- 15. HTTP Status Code ที่ควรรู้
- 16. Build สำหรับ Production
- 17. ตัวอย่างการเพิ่ม Async Controller
- 18. แนวทางจัดโครงสร้างเมื่อระบบใหญ่ขึ้น
- 19. สิ่งที่ควรเพิ่มใน Production API
- 20. Architecture โดยสรุป
- สรุป
#สร้าง REST API ด้วย Express และ TypeScript แบบเป็นระบบ
Express เป็น Web Framework สำหรับ Node.js ที่มีขนาดเล็ก ยืดหยุ่น และนิยมใช้สร้าง REST API ส่วน TypeScript ช่วยเพิ่มระบบ Type ให้กับ JavaScript ทำให้ตรวจพบข้อผิดพลาดได้ตั้งแต่ขั้นตอนพัฒนา ช่วยให้การ Refactor และดูแลโค้ดในโครงการขนาดกลางถึงใหญ่ทำได้ง่ายขึ้น
บทความนี้จะสร้าง REST API สำหรับจัดการข้อมูล Product ด้วย Express 5 + TypeScript พร้อมแนวทางจัดโครงสร้างโปรเจกต์, Validation, Error Handling, Environment Variables และการ Build สำหรับใช้งานจริง
Express 5 ต้องการ Node.js 18 ขึ้นไป สำหรับโปรเจกต์ใหม่แนะนำให้ใช้ Node.js รุ่นที่ยังอยู่ในช่วงสนับสนุน และใช้ TypeScript แบบ
strict
#สิ่งที่เราจะสร้าง
API ตัวอย่างมี Endpoint ดังนี้
| Method | Endpoint | หน้าที่ |
|---|---|---|
| GET | /health |
ตรวจสอบสถานะ API |
| GET | /api/products |
ดูสินค้าทั้งหมด |
| GET | /api/products/:id |
ดูสินค้าตาม ID |
| POST | /api/products |
เพิ่มสินค้า |
| PATCH | /api/products/:id |
แก้ไขสินค้า |
| DELETE | /api/products/:id |
ลบสินค้า |
ข้อมูลตัวอย่างจะเก็บในหน่วยความจำก่อน เพื่อให้เห็นโครงสร้างของ Express API ชัดเจน จากนั้นสามารถเปลี่ยน Repository/Data Layer ไปใช้ MySQL, PostgreSQL, MongoDB หรือ ORM เช่น Prisma ได้ภายหลัง
#1. สร้างโปรเจกต์
สร้างโฟลเดอร์ใหม่
mkdir express-typescript-api
cd express-typescript-api
สร้าง package.json
npm init -y
ติดตั้ง Express, Zod และ dotenv
npm install express zod dotenv
ติดตั้งเครื่องมือสำหรับ TypeScript
npm install -D typescript tsx @types/node @types/express
แพ็กเกจหลักที่ใช้มีหน้าที่ดังนี้
express— HTTP server, routing และ middlewarezod— ตรวจสอบและแปลงข้อมูลที่รับเข้ามาdotenv— อ่าน Environment Variables จากไฟล์.envtypescript— TypeScript compilertsx— รัน TypeScript ระหว่างพัฒนาโดยไม่ต้อง build ก่อน@types/node— Type definitions สำหรับ Node.js@types/express— Type definitions สำหรับ Express
#2. ตั้งค่า package.json
แก้ไข package.json
{
"name": "express-typescript-api",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit"
}
}
คำสั่งสำคัญคือ
npm run dev
ใช้สำหรับ Development และจะ Restart server เมื่อมีการแก้ไขไฟล์
npm run typecheck
ตรวจสอบ Type โดยไม่สร้างไฟล์ JavaScript
npm run build
Compile TypeScript ไปยังโฟลเดอร์ dist
npm start
รัน JavaScript ที่ผ่านการ Build แล้ว
#3. ตั้งค่า TypeScript
สร้างไฟล์ tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*.ts"]
}
จุดสำคัญคือ
strict: trueเปิดการตรวจ Type อย่างเข้มงวดrootDirกำหนด Source CodeoutDirกำหนดตำแหน่งไฟล์หลัง CompileNodeNextทำให้การจัดการ Module สอดคล้องกับระบบ ESM ของ Node.js
เมื่อใช้ NodeNext และ "type": "module" การ import ไฟล์ภายในโปรเจกต์ควรเขียนนามสกุลเป็น .js เช่น
import app from "./app.js";
แม้ไฟล์ต้นฉบับจริงจะเป็น app.ts เพราะหลัง Compile แล้ว Node.js จะเรียกใช้ไฟล์ JavaScript
#4. โครงสร้างโปรเจกต์
สร้างโครงสร้างดังนี้
express-typescript-api/
├── src/
│ ├── controllers/
│ │ └── product.controller.ts
│ ├── middleware/
│ │ └── error.middleware.ts
│ ├── routes/
│ │ └── product.routes.ts
│ ├── schemas/
│ │ └── product.schema.ts
│ ├── types/
│ │ └── product.ts
│ ├── app.ts
│ └── server.ts
├── .env
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json
แนวคิดคือแยกความรับผิดชอบออกจากกัน
Request
│
▼
Route
│
▼
Controller
│
├── Validation
│
▼
Business / Data Logic
│
▼
Response
เมื่อระบบใหญ่ขึ้น สามารถเพิ่ม services/, repositories/, database/ และ config/ ได้
#5. สร้าง Type สำหรับ Product
สร้างไฟล์
src/types/product.ts
export interface Product {
id: string;
name: string;
price: number;
stock: number;
createdAt: string;
}
การมี Type ชัดเจนช่วยให้ IDE และ TypeScript ตรวจสอบว่า Object ของเรามีข้อมูลครบตามที่กำหนด
#6. สร้าง Schema สำหรับ Validation
สร้างไฟล์
src/schemas/product.schema.ts
import { z } from "zod";
export const createProductSchema = z.object({
name: z.string().trim().min(2).max(100),
price: z.number().positive(),
stock: z.number().int().min(0)
});
export const updateProductSchema = createProductSchema
.partial()
.refine((data) => Object.keys(data).length > 0, {
message: "At least one field is required"
});
export type CreateProductInput = z.infer<typeof createProductSchema>;
export type UpdateProductInput = z.infer<typeof updateProductSchema>;
ตัวอย่างข้อมูลที่ผ่าน Validation
{
"name": "Mechanical Keyboard",
"price": 2590,
"stock": 10
}
ตัวอย่างที่ไม่ผ่าน
{
"name": "",
"price": -100,
"stock": -5
}
Zod มีประโยชน์เพราะ TypeScript ตรวจสอบ Type ได้เฉพาะตอน Compile แต่ข้อมูลจาก HTTP Request เป็นข้อมูล Runtime จึงยังต้องมี Runtime Validation
#7. สร้าง Controller
สร้างไฟล์
src/controllers/product.controller.ts
import { randomUUID } from "node:crypto";
import type { RequestHandler } from "express";
import {
createProductSchema,
updateProductSchema
} from "../schemas/product.schema.js";
import type { Product } from "../types/product.js";
let products: Product[] = [
{
id: randomUUID(),
name: "Mechanical Keyboard",
price: 2590,
stock: 10,
createdAt: new Date().toISOString()
},
{
id: randomUUID(),
name: "Wireless Mouse",
price: 1290,
stock: 25,
createdAt: new Date().toISOString()
}
];
export const getProducts: RequestHandler = (_req, res) => {
res.json({
data: products
});
};
export const getProductById: RequestHandler = (req, res) => {
const product = products.find(
(item) => item.id === req.params.id
);
if (!product) {
res.status(404).json({
message: "Product not found"
});
return;
}
res.json({
data: product
});
};
export const createProduct: RequestHandler = (req, res) => {
const result = createProductSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
message: "Validation failed",
errors: result.error.flatten()
});
return;
}
const product: Product = {
id: randomUUID(),
...result.data,
createdAt: new Date().toISOString()
};
products.push(product);
res.status(201).json({
message: "Product created",
data: product
});
};
export const updateProduct: RequestHandler = (req, res) => {
const index = products.findIndex(
(item) => item.id === req.params.id
);
if (index === -1) {
res.status(404).json({
message: "Product not found"
});
return;
}
const result = updateProductSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
message: "Validation failed",
errors: result.error.flatten()
});
return;
}
const currentProduct = products[index];
if (!currentProduct) {
res.status(404).json({
message: "Product not found"
});
return;
}
const updatedProduct: Product = {
...currentProduct,
...result.data
};
products[index] = updatedProduct;
res.json({
message: "Product updated",
data: updatedProduct
});
};
export const deleteProduct: RequestHandler = (req, res) => {
const index = products.findIndex(
(item) => item.id === req.params.id
);
if (index === -1) {
res.status(404).json({
message: "Product not found"
});
return;
}
products.splice(index, 1);
res.status(204).send();
};
ในตัวอย่างนี้เราใช้ RequestHandler จาก Express แทนการเขียน Type ของ req, res และ next ซ้ำทุกฟังก์ชัน
#8. สร้าง Router
สร้างไฟล์
src/routes/product.routes.ts
import { Router } from "express";
import {
createProduct,
deleteProduct,
getProductById,
getProducts,
updateProduct
} from "../controllers/product.controller.js";
const router = Router();
router.get("/", getProducts);
router.get("/:id", getProductById);
router.post("/", createProduct);
router.patch("/:id", updateProduct);
router.delete("/:id", deleteProduct);
export default router;
Express Router ช่วยแบ่ง Endpoint ออกเป็น Module เช่น
/api/products
/api/users
/api/orders
/api/auth
ทำให้ไม่ต้องเขียน Route ทั้งหมดไว้ในไฟล์เดียว
#9. สร้าง 404 และ Error Handling Middleware
สร้างไฟล์
src/middleware/error.middleware.ts
import type {
ErrorRequestHandler,
RequestHandler
} from "express";
export const notFoundHandler: RequestHandler = (req, res) => {
res.status(404).json({
message: `Route not found: ${req.method} ${req.originalUrl}`
});
};
export const errorHandler: ErrorRequestHandler = (
error,
_req,
res,
next
) => {
if (res.headersSent) {
next(error);
return;
}
console.error(error);
res.status(500).json({
message: "Internal server error"
});
};
Error-handling middleware ของ Express ต้องมี Signature 4 Parameters
(error, req, res, next)
และควรวางไว้หลัง Route/Middleware อื่นทั้งหมด
Express 5 สามารถส่ง Error จาก Promise หรือ async route handler ที่ reject/throw ไปยัง Error Middleware ได้โดยอัตโนมัติ จึงทำให้การเขียน asynchronous controller สะดวกกว่า Express 4
#10. สร้าง Express Application
สร้างไฟล์
src/app.ts
import express from "express";
import {
errorHandler,
notFoundHandler
} from "./middleware/error.middleware.js";
import productRouter from "./routes/product.routes.js";
const app = express();
app.disable("x-powered-by");
app.use(express.json());
app.get("/health", (_req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString()
});
});
app.use("/api/products", productRouter);
app.use(notFoundHandler);
app.use(errorHandler);
export default app;
express.json() เป็น Middleware สำหรับอ่าน JSON Request Body
ตัวอย่าง Request
POST /api/products
Content-Type: application/json
{
"name": "USB-C Hub",
"price": 1490,
"stock": 15
}
เมื่อ Express parse สำเร็จ เราจะเข้าถึงข้อมูลผ่าน
req.body
#11. สร้าง Server
สร้างไฟล์
src/server.ts
import "dotenv/config";
import app from "./app.js";
const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
console.log(`API running at http://localhost:${port}`);
});
สร้างไฟล์ .env
PORT=3000
NODE_ENV=development
และสร้าง .env.example
PORT=3000
NODE_ENV=development
#12. สร้าง .gitignore
node_modules
dist
.env
*.log
.DS_Store
ไม่ควร Commit .env หากภายในมี Password, API Key, JWT Secret หรือ Database Connection String
#13. รัน API
ใช้คำสั่ง
npm run dev
ผลลัพธ์
API running at http://localhost:3000
ทดสอบ Health Check
curl http://localhost:3000/health
ตัวอย่าง Response
{
"status": "ok",
"timestamp": "2026-09-15T00:00:00.000Z"
}
#14. ทดสอบ REST API
#ดูสินค้าทั้งหมด
curl http://localhost:3000/api/products
Response
{
"data": [
{
"id": "uuid",
"name": "Mechanical Keyboard",
"price": 2590,
"stock": 10,
"createdAt": "2026-09-15T00:00:00.000Z"
}
]
}
#เพิ่มสินค้า
curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "USB-C Hub",
"price": 1490,
"stock": 15
}'
Response
{
"message": "Product created",
"data": {
"id": "uuid",
"name": "USB-C Hub",
"price": 1490,
"stock": 15,
"createdAt": "2026-09-15T00:00:00.000Z"
}
}
#ดูสินค้าตาม ID
curl http://localhost:3000/api/products/PRODUCT_ID
#แก้ไขสินค้า
curl -X PATCH http://localhost:3000/api/products/PRODUCT_ID \
-H "Content-Type: application/json" \
-d '{
"price": 1390,
"stock": 20
}'
#ลบสินค้า
curl -X DELETE \
http://localhost:3000/api/products/PRODUCT_ID
หากลบสำเร็จ API จะตอบกลับด้วย HTTP Status
204 No Content
#15. HTTP Status Code ที่ควรรู้
| Status | ความหมาย | ตัวอย่าง |
|---|---|---|
200 OK |
Request สำเร็จ | GET / PATCH |
201 Created |
สร้าง Resource สำเร็จ | POST |
204 No Content |
สำเร็จโดยไม่มี Response Body | DELETE |
400 Bad Request |
ข้อมูลที่ส่งมาไม่ถูกต้อง | Validation Error |
401 Unauthorized |
ยังไม่ได้ Authentication | JWT ไม่ถูกต้อง |
403 Forbidden |
ไม่มีสิทธิ์เข้าถึง | Role ไม่อนุญาต |
404 Not Found |
ไม่พบ Resource | Product ID ไม่มี |
409 Conflict |
ข้อมูลขัดแย้ง | Email ซ้ำ |
500 Internal Server Error |
Error ภายใน Server | Unexpected Error |
#16. Build สำหรับ Production
ตรวจ Type
npm run typecheck
Build
npm run build
จะได้โฟลเดอร์
dist/
├── controllers/
├── middleware/
├── routes/
├── schemas/
├── types/
├── app.js
└── server.js
จากนั้นรัน
npm start
หรือ
NODE_ENV=production node dist/server.js
#17. ตัวอย่างการเพิ่ม Async Controller
ในการใช้งานจริง เรามักต้องเรียก Database หรือ External API ซึ่งเป็นงานแบบ asynchronous
Express 5 รองรับ async handler และส่ง rejected Promise ไปยัง Error Handler โดยอัตโนมัติ
ตัวอย่าง
router.get("/:id", async (req, res) => {
const product = await database.product.findById(req.params.id);
if (!product) {
res.status(404).json({
message: "Product not found"
});
return;
}
res.json({
data: product
});
});
หาก database.product.findById() throw Error หรือ Promise reject, Express 5 จะส่ง Error ต่อไปยัง Error-handling middleware
จุดนี้เป็นหนึ่งในความเปลี่ยนแปลงสำคัญของ Express 5 เมื่อเทียบกับ Express 4
#18. แนวทางจัดโครงสร้างเมื่อระบบใหญ่ขึ้น
เมื่อ API เริ่มมี Business Logic มากขึ้น สามารถปรับเป็นโครงสร้าง
src/
├── config/
├── controllers/
├── middleware/
├── repositories/
├── routes/
├── schemas/
├── services/
├── types/
├── utils/
├── app.ts
└── server.ts
Flow อาจเปลี่ยนเป็น
HTTP Request
│
▼
Router
│
▼
Middleware / Validation
│
▼
Controller
│
▼
Service
│
▼
Repository
│
▼
Database
แนวทางนี้ช่วยแยก
- HTTP concern
- Business logic
- Data access
- Validation
- Infrastructure
ออกจากกัน ทำให้ Test และ Maintain ได้ง่ายขึ้น
#19. สิ่งที่ควรเพิ่มใน Production API
REST API สำหรับ Production ควรพิจารณาเพิ่มองค์ประกอบต่อไปนี้
#Database
เช่น
- PostgreSQL
- MySQL
- MongoDB
พร้อม Data Access Layer เช่น Prisma หรือ ORM/Query Builder ที่เหมาะกับระบบ
#Authentication และ Authorization
เช่น
- JWT
- Session
- OAuth 2.0
- OpenID Connect
#Security
เช่น
- CORS policy
- Helmet
- Rate Limiting
- Request size limit
- Input validation
- Secret management
#Logging
ควรใช้ Structured Logging เช่น
{
"level": "info",
"method": "POST",
"path": "/api/products",
"status": 201,
"durationMs": 18
}
#Automated Testing
ควรมี
- Unit Test
- Integration Test
- API Test
- End-to-End Test
เครื่องมือที่สามารถใช้ได้ เช่น
- Vitest
- Jest
- Supertest
- Playwright API Testing
- Postman / Newman
#API Documentation
สามารถเพิ่ม OpenAPI / Swagger เพื่อให้ Frontend Developer หรือระบบอื่นเข้าใจ Contract ของ API
ตัวอย่าง Endpoint
/api/products:
get:
summary: Get all products
post:
summary: Create product
#Observability
ระบบ Production ควรมีอย่างน้อย
- Logs
- Metrics
- Traces
- Health Check
และสามารถเชื่อมต่อกับ OpenTelemetry, Prometheus หรือระบบ Monitoring อื่นได้
#20. Architecture โดยสรุป
Client
│
│ HTTP / JSON
▼
Express Router
│
▼
Validation
│
▼
Controller
│
▼
Service
│
▼
Repository
│
▼
Database
Express ไม่บังคับ Architecture ทำให้นักพัฒนาสามารถเริ่มต้นด้วยโครงสร้างเล็ก ๆ แล้วค่อยเพิ่ม Layer ตามความซับซ้อนของระบบ
#สรุป
การใช้ Express + TypeScript เป็นทางเลือกที่เหมาะสำหรับการสร้าง REST API ที่ต้องการความเรียบง่ายและควบคุม Architecture ได้เอง
หัวใจสำคัญของโปรเจกต์ไม่ได้อยู่เพียงแค่การสร้าง Route แต่ควรออกแบบให้มี
- TypeScript แบบ
strict - Routing ที่แบ่งตาม Domain
- Runtime Validation สำหรับข้อมูลจาก Client
- Error Handling แบบรวมศูนย์
- Environment Variables
- แยก Controller และ Business Logic
- Automated Testing
- Security และ Observability สำหรับ Production
เมื่อพื้นฐานเหล่านี้พร้อม เราสามารถต่อยอด API เดิมไปใช้ PostgreSQL/MySQL, Prisma, JWT, OpenAPI, Docker และ CI/CD ได้โดยไม่ต้องเปลี่ยนแนวคิดหลักของระบบทั้งหมด
#เอกสารอ้างอิง
- Express — Installing: https://expressjs.com/en/starter/installing.html
- Express — Routing: https://expressjs.com/en/guide/routing.html
- Express — Middleware: https://expressjs.com/en/guide/using-middleware.html
- Express — Error Handling: https://expressjs.com/en/guide/error-handling.html
- Express — Migrating to Express 5: https://expressjs.com/en/guide/migrating-5.html
- TypeScript — TSConfig Reference: https://www.typescriptlang.org/tsconfig/
- Zod: https://zod.dev/