#สร้าง 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 และ middleware
  • zod — ตรวจสอบและแปลงข้อมูลที่รับเข้ามา
  • dotenv — อ่าน Environment Variables จากไฟล์ .env
  • typescript — TypeScript compiler
  • tsx — รัน 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 Code
  • outDir กำหนดตำแหน่งไฟล์หลัง Compile
  • NodeNext ทำให้การจัดการ 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 แต่ควรออกแบบให้มี

  1. TypeScript แบบ strict
  2. Routing ที่แบ่งตาม Domain
  3. Runtime Validation สำหรับข้อมูลจาก Client
  4. Error Handling แบบรวมศูนย์
  5. Environment Variables
  6. แยก Controller และ Business Logic
  7. Automated Testing
  8. Security และ Observability สำหรับ Production

เมื่อพื้นฐานเหล่านี้พร้อม เราสามารถต่อยอด API เดิมไปใช้ PostgreSQL/MySQL, Prisma, JWT, OpenAPI, Docker และ CI/CD ได้โดยไม่ต้องเปลี่ยนแนวคิดหลักของระบบทั้งหมด


#เอกสารอ้างอิง