Engineering Standard

Coding Standard สำหรับ reverse proxy microservice stack

มาตรฐานนี้ตั้งใจให้ทีมเพิ่ม service ใหม่ แก้ routing และ debug production ได้ง่าย โดยใช้ contract เดียวกันทั้ง Next.js, Go, Python/FastAPI, nginx และ Docker Compose.

Core Principles

One route contract ทุก app เปิดใต้ /projects/<slug> และ listen ใน container ที่ port 3100.
Base path aware app ต้องสร้าง link, asset, API URL และ redirect ที่ไม่หลุดจาก base path.
Config over hardcode base path, port, root path และ public URL ต้องมาจาก env หรือ helper กลาง.
Small replaceable services service ใหม่ควรเพิ่มด้วย compose + nginx location + portal link โดยไม่แตะ service อื่น.

Repository Layout

templates/
  README.md
  add-service-checklist.md
  nginx/project-location.conf
  portal/card.html
  nextjs/
  vuejs/
  nuxt/
  nestjs/
  fastapi/
  golang/

services/
  docker-compose.yml
  docker-compose.local.yml
  nginx/
    nginx.conf
    conf.d/
      default.conf
      local.conf
  portal/
    index.html
  projects/
    demo1/
    task-assignee/
  templates/
    nextjs.compose.yml
    fastapi.compose.yml
    go.compose.yml
    microservice.compose.yml

root repo ใช้เก็บเอกสารระดับทีม เช่น README.md, AGENTS.md, handbook.html, deployment.html, standard.html และ starter scaffolds ใน templates/.

Routing Contract

Layer Standard Reason
Public path /projects/<slug>/ ทำให้ portal, nginx และ app config สื่อสารด้วย key เดียว
Container port 3100 ลด port mismatch และทำให้ nginx location เพิ่มง่าย
Network <DOCKER_NETWORK> nginx resolve service name ผ่าน Docker DNS
Forwarded prefix X-Forwarded-Prefix: /projects/<slug> framework ที่รองรับ prefix จะ generate URL ได้ถูก

Next.js Standard

Config

const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "/projects/my-app";

const nextConfig = {
  output: "standalone",
  basePath,
  assetPrefix: basePath || undefined
};

Links and URLs

ใช้ next/link สำหรับ internal route ปกติ. ถ้าใช้ raw <a>, static public file, redirect, หรือ fetch ใน browser ต้องผ่าน helper กลาง เช่น withBasePath("/portal/plan").

import { withBasePath } from "@/lib/base-path";

await fetch(withBasePath("/api/db/events"), { method: "POST" });
<a href={withBasePath("/portal/plan")}>Research Plan</a>
ห้ามเขียน browser-facing URL แบบ href="/portal/plan" หรือ fetch("/api/...") ใน app ที่ mount ใต้ subpath.

Component Style

Vue.js / Vite Standard

Vue.js services use Vite and must set base from VITE_BASE_PATH. Use a helper for raw anchors or browser fetches.

const base = process.env.VITE_BASE_PATH || "/projects/vue-demo/";

export default defineConfig({
  base,
  plugins: [vue()]
});
environment:
  PORT: 3100
  VITE_BASE_PATH: /projects/vue-demo/

Nuxt Standard

Nuxt services use NUXT_APP_BASE_URL so routes, assets, and API calls stay under /projects/<slug>/.

export default defineNuxtConfig({
  app: {
    baseURL: process.env.NUXT_APP_BASE_URL || "/projects/nuxt-demo/"
  }
});
environment:
  NITRO_HOST: 0.0.0.0
  NITRO_PORT: 3100
  NUXT_APP_BASE_URL: /projects/nuxt-demo/

NestJS Standard

NestJS APIs must honor BASE_PATH. The template uses app.setGlobalPrefix(), so nginx can preserve the external prefix.

const prefix = (process.env.BASE_PATH || "")
  .replace(/^\/+/, "")
  .replace(/\/+$/, "");

if (prefix) {
  app.setGlobalPrefix(prefix);
}
environment:
  PORT: 3100
  BASE_PATH: /projects/nestjs-demo

Python / FastAPI Standard

Runtime

services:
  fastapi-demo:
    expose:
      - "3100"
    environment:
      PORT: 3100
      ROOT_PATH: /projects/fastapi-demo
    command: >
      uvicorn main:app
      --host 0.0.0.0
      --port 3100
      --proxy-headers
      --forwarded-allow-ips="*"
      --root-path /projects/fastapi-demo

Code

import os
from fastapi import FastAPI

app = FastAPI(root_path=os.getenv("ROOT_PATH", ""))

@app.get("/healthz")
def healthz() -> dict[str, str]:
    return {"status": "ok"}

Golang Standard

Runtime

services:
  go-demo:
    expose:
      - "3100"
    environment:
      PORT: 3100
      BASE_PATH: /projects/go-demo

Code

package main

import (
    "log/slog"
    "net/http"
    "os"
)

func main() {
    port := getenv("PORT", "3100")
    basePath := getenv("BASE_PATH", "")

    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte(`{"status":"ok"}`))
    })

    slog.Info("server starting", "port", port, "basePath", basePath)
    if err := http.ListenAndServe(":"+port, mux); err != nil {
        slog.Error("server stopped", "error", err)
        os.Exit(1)
    }
}

func getenv(key, fallback string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return fallback
}

Backend + PostgreSQL Standard

เมื่อ backend ต้องต่อ PostgreSQL ใน Docker Compose ให้ต่อผ่าน Docker service name ไม่ใช่ localhost.

Compose DB Service

<DB_SERVICE>:
  image: postgres:16-alpine
  container_name: <DB_SERVICE>
  restart: unless-stopped
  environment:
    POSTGRES_DB: <DB_NAME>
    POSTGRES_USER: <DB_USER>
    POSTGRES_PASSWORD: <DB_PASSWORD_ENV>
  volumes:
    - <DB_SERVICE>-data:/var/lib/postgresql/data
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U <DB_USER> -d <DB_NAME>"]
    interval: 10s
    timeout: 5s
    retries: 5
  expose:
    - "5432"
  networks:
    - <DOCKER_NETWORK>

Backend Connection

<APP_SERVICE>:
  environment:
    DATABASE_URL: postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>
  depends_on:
    <DB_SERVICE>:
      condition: service_healthy
ใน Docker network ให้ใช้ host <DB_SERVICE> หรือชื่อ service ของ Postgres. ห้ามใช้ localhost จาก backend container.

Framework Usage

# FastAPI / SQLAlchemy / asyncpg DATABASE_URL=postgresql+asyncpg://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME> # NestJS / TypeORM / Prisma / node-postgres DATABASE_URL=postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME> # Go / pgx DATABASE_URL=postgres://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>?sslmode=disable

Data Safety

nginx Standard

File Boundaries

Project Location Pattern

location /projects/demo1/ {
  set $demo1_upstream <APP_SERVICE>:3100;
  set $project_prefix /projects/demo1;
  proxy_pass http://$demo1_upstream;
}
ถ้า app ตั้ง base path แล้ว ห้ามใส่ slash ท้าย upstream แบบ proxy_pass http://service:3100/; เพราะจะ strip prefix.

Docker Compose Standard

services:
  my-demo:
    build:
      context: ./projects/my-demo
    restart: unless-stopped
    expose:
      - "3100"
    environment:
      PORT: 3100
      PUBLIC_BASE_PATH: /projects/my-demo
    networks:
      - <DOCKER_NETWORK>
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Docker Compose + Portal Setup Step By Step

ใช้ขั้นตอนนี้ทุกครั้งที่เอา service ใหม่เข้า stack. ตัวอย่างใช้ slug reports-demo.

Step 1: เลือก slug

reports-demo
/projects/reports-demo/

slug ต้องเป็น lowercase kebab-case และควรตรงกับ Docker service name.

Step 2: copy template

cp -R templates/nextjs services/projects/reports-demo
# หรือเลือก templates/vuejs, templates/nuxt, templates/nestjs, templates/fastapi, templates/golang

Step 3: เพิ่ม service ใน docker-compose.yml

reports-demo:
  build:
    context: <DEPLOY_PATH>/projects/reports-demo
  container_name: reports-demo
  restart: unless-stopped
  expose:
    - "3100"
  environment:
    PORT: 3100
    PUBLIC_BASE_PATH: /projects/reports-demo
  networks:
    - <DOCKER_NETWORK>
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"

ใช้ expose ไม่ใช้ host ports สำหรับ app service ที่อยู่หลัง nginx.

Step 4: ตั้ง framework base path

# Next.js
NEXT_PUBLIC_BASE_PATH: /projects/reports-demo

# Vue.js / Vite
VITE_BASE_PATH: /projects/reports-demo/

# Nuxt
NUXT_APP_BASE_URL: /projects/reports-demo/

# NestJS / Go
BASE_PATH: /projects/reports-demo

# FastAPI
ROOT_PATH: /projects/reports-demo

Step 5: เพิ่ม depends_on ให้ reverse-proxy service

<NGINX_SERVICE>:
  depends_on:
    <APP_SERVICE>:
      condition: service_started

ถ้า service นั้น run จาก compose แยก ให้ไม่ต้องเพิ่ม depends_on แต่ต้องอยู่ network เดียวกัน.

Step 6: เพิ่ม PostgreSQL ถ้า backend ต้องใช้ DB

<DB_SERVICE>:
  image: postgres:16-alpine
  environment:
    POSTGRES_DB: <DB_NAME>
    POSTGRES_USER: <DB_USER>
    POSTGRES_PASSWORD: <DB_PASSWORD_ENV>
  volumes:
    - <DB_SERVICE>-data:/var/lib/postgresql/data
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U <DB_USER> -d <DB_NAME>"]
  expose:
    - "5432"
  networks:
    - <DOCKER_NETWORK>

reports-demo:
  environment:
    DATABASE_URL: postgresql://<DB_USER>:<DB_PASSWORD_ENV>@<DB_SERVICE>:5432/<DB_NAME>
  depends_on:
    <DB_SERVICE>:
      condition: service_healthy

backend ต้องใช้ host <DB_SERVICE> ไม่ใช่ localhost.

Step 7: เพิ่ม nginx route

location = /projects/reports-demo {
  set $reports_demo_upstream <APP_SERVICE>:3100;
  set $project_prefix /projects/reports-demo;
  proxy_pass http://$reports_demo_upstream;
}

location /projects/reports-demo/ {
  set $reports_demo_upstream <APP_SERVICE>:3100;
  set $project_prefix /projects/reports-demo;
  proxy_pass http://$reports_demo_upstream;
}
ห้ามใช้ proxy_pass http://<APP_SERVICE>:3100/; กับ base-path-aware app เพราะ slash ท้าย upstream จะ strip prefix.

Step 8: เพิ่ม portal card

<a class="card" href="/projects/reports-demo/">
  <div class="card-header">
    <span class="card-badge">Demo</span>
    <span class="card-status">Ready</span>
  </div>
  <h3>Reports Demo</h3>
  <p>Short description of what this service demonstrates.</p>
  <code>/projects/reports-demo/</code>
</a>

Step 9: validate

docker compose -f services/docker-compose.yml config
docker exec <NGINX_CONTAINER> nginx -t
docker exec <NGINX_CONTAINER> nginx -s reload
curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' https://<PUBLIC_APP_HOST>/projects/reports-demo/
curl -sL -o /dev/null -w '%{http_code} %{url_effective}\n' https://<PUBLIC_APP_HOST>/projects/reports-demo/healthz

สำหรับรายละเอียดเต็ม ใช้ templates/docker-compose-portal-steps.md และ templates/postgres/README.md.

Templates Standard

templates/ คือ source of truth สำหรับเริ่ม service ใหม่. อย่า copy จาก app เก่าโดยตรงถ้า app นั้นมี history หรือ legacy config.

Template Use For Includes
templates/nextjs Next.js App Router service standalone Dockerfile, basePath config, withBasePath, health route
templates/vuejs Vue.js Vite service Vite base config, Dockerfile, base path helper example
templates/nuxt Nuxt SSR service Nuxt baseURL config, Nitro port env, health API
templates/nestjs NestJS API service global prefix from BASE_PATH, Dockerfile, health route
templates/postgres Backend database service Postgres compose pattern, env example, init schema, healthcheck
templates/fastapi Python API service FastAPI root_path, Uvicorn command, health route
templates/golang Go HTTP service Dockerfile, env-driven port/base path, health route
templates/nginx/project-location.conf New reverse proxy route prefix-preserving locations with $project_prefix
templates/portal/card.html Portal app card standard link to /projects/<slug>/
ทุก template ต้องใช้ port 3100, base path /projects/<slug>, และ Docker network <DOCKER_NETWORK>.

Quality Gates

Quality gate คือด่านผ่าน/ไม่ผ่านก่อน merge หรือ deploy. งาน production ต้องไม่จบแค่ container start ได้ แต่ต้องผ่าน dependency CVE, static analysis, image scan, build, nginx และ smoke route.

Area Command Expected
npm CVE npm audit --audit-level=moderate 0 vulnerability for runtime Next.js apps
SonarQube docker compose --profile quality up -d sonarqube sonar-postgres then run scanner Quality Gate passed; no new blocker/critical issue
Container CVE trivy image <image> or docker scout cves <image> no fixable high/critical CVE before deploy
Local gate script ./scripts/quality-gate.sh npm audit, Next build, compose config and Trivy filesystem scan pass
Service smoke script ./scripts/check-service.sh <slug> route, healthz และ base path asset check ผ่าน
Compose docker compose -f services/docker-compose.local.yml config config renders without error
nginx nginx -t inside nginx container syntax is ok and test is successful
Next.js npm run build compile and TypeScript pass
Route curl -sL -o /dev/null -w '%{http_code}' http://localhost/projects/demo1/ 200
Assets grep /projects/demo1/_next assets are base-path prefixed
Pipeline monitor curl -I http://localhost/pipeline/ portal can show deployment, gate and rollback checklist
Reference: npm audit, Trivy/Docker Scout และ SonarQube Quality Gate ต้องอยู่ใน CI/CD pipeline ไม่ใช่ทำเฉพาะตอนเกิด incident.

Standard Severity: MUST / SHOULD / MAY

MUST
  • ทุก service ต้องอยู่ใต้ /projects/<slug>/.
  • Container ต้องฟังบน port 3100.
  • Browser URL และ asset ต้องใช้ base path helper เสมอ.
  • Backend ระหว่าง container ติดต่อด้วย Docker service name ไม่ใช้ localhost.
  • ทุก service ต้องมี /healthz และ log ที่ติด requestId.
  • ห้าม commit secret, token, password หรือ private key.
SHOULD
  • ใช้ template ล่าสุดก่อนสร้าง service ใหม่.
  • มี structured logging, correlation id, และ error schema เดียวกัน.
  • มี compose healthcheck + rollback note.
  • มี docs/flow, lab, และ checklist ที่ team ใช้ซ้ำได้.
MAY
  • เพิ่ม framework helper (Auth helper, API client helper) ตามทีม.
  • เพิ่ม local script ช่วยตรวจ route/asset.
  • เพิ่ม fixture สำหรับ training และ demo.

Role-based Standards

Role ต้องอ่าน ต้องทำได้
Frontend Dev Routing contract, FE architecture, Next.js/Nuxt/Vue standard ทำ base path helper ได้, แก้ link/fetch หลุด, เปิด dev server ผ่าน subpath
Backend Dev API standard, Migration, PostgreSQL pattern, logging ทำ /healthz, validation schema, request tracing, migrate safely
DevOps Nginx, compose, deploy flow, rollback, maintenance เพิ่ม route, debug 502/504, validate healthz, สรุป rollback note
Tech Lead Arch, security, quality gate, PR checklist กำหนด severity, approve exception, enforce DoD ก่อนปล่อยงาน

Bad vs Good Examples (สำคัญมาก)

Bad
// Next.js fetch ที่หลุด base path
fetch('/api/healthz')
Good
fetch(withBasePath('/api/healthz'))
Bad
proxy_pass http://app-service:3100/;
Good
proxy_pass http://app-service:3100;

Definition of Done: New Service

PR Review Checklist

Add New Service Checklist

  1. เลือก slug เช่น reports-demo.
  2. สร้าง app ใต้ services/projects/reports-demo หรือใช้ image ภายนอก.
  3. ตั้ง container port เป็น 3100.
  4. ตั้ง env base path เป็น /projects/reports-demo.
  5. เพิ่ม service ใน compose หรือใช้ template ที่ตรง framework.
  6. เพิ่ม nginx location ที่กำหนด $project_prefix และ upstream.
  7. เพิ่ม portal card ที่ชี้ /projects/reports-demo/.
  8. รัน compose config, nginx test, build, curl route และ asset check.