0

Bài 03: Kiến trúc dữ liệu: Tối ưu /api (OpenAPI/gRPC) và /web (Static/Templates)

1. Mục tiêu bài học

  • Phân định ranh giới giữa giao ước API (API Contracts - /api) và giao diện hiển thị phía client (/web).

  • Tổ chức các file định nghĩa giao tiếp đa giao thức (Protocol Buffers, OpenAPI/Swagger specs, JSON schemas).

  • Xây dựng luồng tích hợp UI: Quản lý SPA bundle (React/Vue) hoặc Server-Side Rendering (Go html/template).

2. Nội dung chi tiết

  • Phần 1: Thư mục /api – Nơi lưu trữ giao ước hệ thống

    • Định nghĩa bản chất: /api không chứa mã nguồn xử lý HTTP handler mà chứa tài liệu và định nghĩa schema trung lập (Protobuf .proto, OpenAPI .yaml, GraphQL .graphql).

    • Quy tắc đánh phiên bản (Versioning): Đặt thư mục theo cấu trúc /api/proto/v1/, /api/openapi/v2/.

    • Chiến lược code generation: Đặt file sinh mã (*.pb.go, Swagger models) ở đâu? Phân tích 2 trường hợp: sinh trực tiếp vào /api vs đưa vào /internal/generated.

  • Phần 2: Thư mục /web – Xử lý giao diện người dùng

    • Sự khác biệt cốt lõi giữa /assets (tài nguyên backend thuần túy: email template, fonts, seed data) và /web (giao diện web phục vụ người dùng cuối).

    • Hai kịch bản kiến trúc phổ biến:

      • Kịch bản A: Go render trực tiếp bằng server-side template (/web/template, /web/static).

      • Kịch bản B: Go làm host chứa SPA bundle đã build sẵn từ frontend team (/web/app/dist).

  • Phần 3: Tích hợp và phục vụ tài nguyên web

    • Cấu hình router phân luồng HTTP: /api/* cho backend services và /* cho static assets/templates.

3. Cấu trúc thư mục minh họa

Plaintext

ecommerce-core/
├── api/
│   ├── openapi/
│   │   └── v1/
│   │       └── product.yaml     # OpenAPI Specification
│   └── proto/
│       └── v1/
│           └── product.proto    # gRPC Definition
├── web/
│   ├── static/
│   │   ├── css/
│   │   │   └── style.css
│   │   └── js/
│   │       └── app.js
│   └── template/
│       └── index.html           # Server-side HTML template
├── cmd/
│   └── server/
│       └── main.go
├── go.mod
└── go.sum

4. Ví dụ code minh họa

api/proto/v1/product.proto (Định nghĩa giao ước gRPC):

Protocol Buffers

syntax = "proto3";

package product.v1;

option go_package = "ecommerce-core/internal/gen/product/v1;productv1";

service ProductService {
  rpc GetProduct (GetProductRequest) returns (GetProductResponse);
}

message GetProductRequest {
  string id = 1;
}

message ProductResponse {
  string id = 1;
  string name = 2;
  double price = 3;
}

web/template/index.html:

HTML

<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <title>Cửa hàng trực tuyến</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
    <h1>Danh sách sản phẩm</h1>
    <p>Chào mừng bạn đến với hệ thống backend viết bằng Go!</p>
</body>
</html>

cmd/server/main.go (Cấu hình route phục vụ cả giao ước OpenAPI, static assets và template):

Go

package main

import (
	"html/template"
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	// 1. Phục vụ tài liệu OpenAPI từ /api
	apiDocFS := http.FileServer(http.Dir("./api/openapi"))
	mux.Handle("/docs/", http.StripPrefix("/docs/", apiDocFS))

	// 2. Phục vụ file tĩnh (CSS/JS) từ /web/static
	staticFS := http.FileServer(http.Dir("./web/static"))
	mux.Handle("/static/", http.StripPrefix("/static/", staticFS))

	// 3. Phục vụ template HTML từ /web/template
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		tmpl, err := template.ParseFiles("./web/template/index.html")
		if err != nil {
			http.Error(w, "Lỗi server nội bộ", http.StatusInternalServerError)
			return
		}
		_ = tmpl.Execute(w, nil)
	})

	log.Println("Server lắng nghe trên cổng :8080...")
	_ = http.ListenAndServe(":8080", mux)
}

Bài 06: Vận hành và tự động hóa: Tổ chức thư mục /scripts, /deployments, /build

1. Mục tiêu bài học

  • Tách biệt logic mã nguồn ứng dụng khỏi quy trình đóng gói, tự động hóa và triển khai hạ tầng.

  • Thiết kế kịch bản CI/CD, Makefile, shell scripts chuẩn mực trong /scripts.

  • Đóng gói Docker đa giai đoạn (Multi-stage build) trong /build và thiết lập hạ tầng Kubernetes/Docker Compose trong /deployments.

2. Nội dung chi tiết

  • Phần 1: Tự động hóa tác vụ với /scripts

    • Trách nhiệm: Chứa kịch bản build, test linting, kiểm tra bảo mật, database migration và code generation.

    • Phối hợp Makefile ở thư mục gốc và các file script thực thi phức tạp: Viết logic ngắn gọn trong Makefile, chuyển logic phân nhánh shell phức tạp vào /scripts/*.sh.

    • Đảm bảo tính di động: Sử dụng shell POSIX chuẩn (#!/usr/bin/env bash), quản lý quyền cấp phép thực thi chmod +x.

  • Phần 2: Đóng gói và phát hành với /build

    • Mục đích: Lưu trữ cấu hình đóng gói phần mềm (Packaging & Continuous Integration).

    • Vị trí đặt file Docker: Tránh đặt rải rác ngoài thư mục root nếu có nhiều binary; đưa các cấu hình container và system package (systemd units, RPM/DEB specs) vào /build/package/.

  • Phần 3: Quản lý cấu hình hạ tầng triển khai với /deployments

    • Phân loại hạ tầng: Docker-Compose môi trường local dev, Helm charts, Kubernetes manifests, cấu hình Terraform.

    • Tách bạch môi trường rõ ràng: /deployments/k8s/dev/, /deployments/k8s/prod/.

3. Cấu trúc thư mục minh họa

Plaintext

microservice-hub/
├── build/
│   └── package/
│       ├── api.Dockerfile          # Dockerfile cho Binary API
│       └── worker.Dockerfile       # Dockerfile cho Binary Worker
├── deployments/
│   ├── docker-compose.dev.yaml     # Chạy local testing (DB, Redis, App)
│   └── k8s/
│       ├── deployment.yaml
│       └── service.yaml
├── scripts/
│   ├── generate-proto.sh           # Script tự động generate code từ proto
│   └── lint.sh                     # Script chạy golangci-lint
├── Makefile
├── cmd/
│   └── api/
│       └── main.go
├── go.mod
└── go.sum

4. Ví dụ code minh họa

scripts/generate-proto.sh (Kịch bản sinh mã tự động):

Bash

#!/usr/bin/env bash

set -euo pipefail

PROTO_DIR="api/proto/v1"
OUT_DIR="internal/gen/product/v1"

echo "==> Đang dọn dẹp thư mục code sinh cũ..."
mkdir -p "${OUT_DIR}"

echo "==> Bắt đầu biên dịch Protobuf qua protoc..."
protoc --proto_path="${PROTO_DIR}" \
       --go_out="${OUT_DIR}" --go_opt=paths=source_relative \
       --go-grpc_out="${OUT_DIR}" --go-grpc_opt=paths=source_relative \
       "${PROTO_DIR}"/*.proto

echo "==> Hoàn tất sinh mã gRPC!"

build/package/api.Dockerfile (Multi-stage build tối ưu kích thước image và bảo mật):

Dockerfile

# Stage 1: Build binary bằng Go runtime đầy đủ
FROM golang:1.23-alpine AS builder

WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source code và compile binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/bin/server ./cmd/api/main.go

# Stage 2: Runtime image tối giản từ Scratch hoặc Distroless
FROM gcr.io/distroless/static-debian12:nonroot

WORKDIR /

COPY --from=builder /app/bin/server /server

USER nonroot:nonroot

EXPOSE 8080

ENTRYPOINT ["/server"]

Makefile (Giao diện dòng lệnh trung tâm cho developer):

Makefile

.PHONY: build test lint proto run-dev

APP_NAME ?= api

lint:
	@echo "Đang kiểm tra mã nguồn..."
	@bash ./scripts/lint.sh

proto:
	@echo "Đang sinh mã từ proto files..."
	@bash ./scripts/generate-proto.sh

build:
	@echo "Đang build binary cho $(APP_NAME)..."
	@docker build -f build/package/$(APP_NAME).Dockerfile -t $(APP_NAME):latest .

run-dev:
	@echo "Khởi động môi trường local development..."
	@docker-compose -f deployments/docker-compose.dev.yaml up -d

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí