เชื่อมต่อ Spring Boot API กับ Vanilla HTML/CSS/JS Frontend

โดย CyberMAN


Dev Blog · Spring Boot · Vanilla JS

เชื่อมต่อ Spring Boot API
กับ Vanilla HTML/CSS/JS Frontend

ทำ REST API ให้คุยกับ Frontend แบบ Pure JS ไม่ง้อ Framework — ครอบคลุม CORS, Fetch API และ JWT Authentication

June 2025Spring Boot 3Java 17อ่าน ~8 นาที

🚀 ทำไมต้อง Vanilla JS?

หลายคนอาจสงสัยว่าในยุคที่ React, Vue, Angular ครองตลาด แล้วทำไมเราถึงยังพูดถึง Vanilla HTML/CSS/JS อยู่?

คำตอบง่ายมาก — บางโปรเจกต์ไม่ต้องการ Framework หนักๆ ก็ได้ โดยเฉพาะ Admin Panel เล็กๆ, Prototype, หรือ ระบบภายในองค์กร ที่ต้องส่งมอบเร็ว ไม่มี Build Pipeline ให้ยุ่งยาก

บทความนี้จะพาเดินทางตั้งแต่ต้นจนจบ: ตั้งค่า Spring Boot → เปิด CORS → เรียก API จาก JS → จัดการ Auth แบบ JWT ทั้งหมดโดยไม่ต้องพึ่ง Build Tool ฝั่ง Frontend เลยแม้แต่น้อย


🏗 Architecture Overview

ก่อนลงมือ ดู Big Picture กันก่อน:

  ┌─────────────────┐          ┌──────────────────────────┐
  │  Browser         │  fetch() │  Spring Boot App           │
  │  (HTML/CSS/JS)  │ ───────► │  :8080                    │
  │  :3000 / file   │ ◄─────── │  REST API + Security      │
  └─────────────────┘   JSON   └──────────────────────────┘
                                          │
                               ┌──────────▼──────────┐
                               │  Database (H2/MySQL)  │
                               └─────────────────────┘
⚠️ สำคัญ: Browser และ Spring Boot อาจรันคนละ Port (Cross-Origin) ดังนั้น CORS คือสิ่งแรกที่ต้องจัดการ ถ้าลืมจะเจอ Error ทันที

⚙️ Part 1: ตั้งค่า Spring Boot

1.1 สร้างโปรเจกต์ด้วย Spring Initializr

ไปที่ start.spring.io แล้วเลือก Dependencies ดังนี้:

  • Spring Web
  • Spring Security
  • Spring Data JPA
  • H2 Database (ทดสอบ) หรือ MySQL Driver (Production)
  • Lombok

1.2 ตั้งค่า CORS — หัวใจสำคัญ

CORS (Cross-Origin Resource Sharing) คือกลไกที่ Browser ใช้ตรวจสอบว่าเว็บที่ต้องการเรียก API ได้รับอนุญาตหรือไม่ ถ้าไม่ตั้งค่า จะเจอ Error นี้ทันที:

Access to fetch at 'http://localhost:8080/api/...' from origin 'http://localhost:3000' has been blocked by CORS policy.

แก้ด้วย Global CORS Configuration:

Java@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("http://localhost:3000")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowedHeaders("*")
                    .allowCredentials(true);
            }
        };
    }
}
📌 Production: เปลี่ยน allowedOrigins เป็น domain จริง เช่น https://myapp.com — อย่าใช้ * กับ allowCredentials(true) พร้อมกัน

1.3 สร้าง REST Controller

ตัวอย่าง Product API อย่างง่าย:

Java@RestController
@RequestMapping("/api/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping
    public ResponseEntity<List<Product>> getAll() {
        return ResponseEntity.ok(productService.findAll());
    }

    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
        return ResponseEntity.status(HttpStatus.CREATED)
                            .body(productService.save(product));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        productService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

🌐 Part 2: Vanilla JS Frontend

2.1 โครงสร้าง Project

Structurefrontend/
├── index.html
├── css/
│   └── style.css
└── js/
    ├── api.js        ← ตัวกลางเรียก API ทั้งหมด
    ├── auth.js       ← จัดการ Token
    └── app.js        ← Business Logic / UI

2.2 สร้าง API Client — api.js

แยก Logic การเรียก API ออกมาเป็น Module เดียว ทำให้ดูแลง่าย ไม่ต้องเขียน fetch() ซ้ำทุกที่:

JavaScript// js/api.js
const BASE_URL = 'http://localhost:8080/api';

async function apiFetch(endpoint, options = {}) {
    const token = localStorage.getItem('jwt_token');

    const defaultHeaders = {
        'Content-Type': 'application/json',
        ...(token ? { 'Authorization': `Bearer ${token}` } : {})
    };

    const response = await fetch(`${BASE_URL}${endpoint}`, {
        ...options,
        headers: { ...defaultHeaders, ...options.headers }
    });

    if (response.status === 401) {
        localStorage.removeItem('jwt_token');
        window.location.href = '/login.html'; // Token หมดอายุ
        return;
    }

    if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
    }

    if (response.status === 204) return null; // No Content
    return response.json();
}

// Export ฟังก์ชันสำหรับ Product
const ProductAPI = {
    getAll: () => apiFetch('/products'),
    create: (data) => apiFetch('/products', { method: 'POST', body: JSON.stringify(data) }),
    delete: (id) => apiFetch(`/products/${id}`, { method: 'DELETE' }),
};

2.3 แสดงข้อมูลใน HTML

HTML<!-- index.html -->
<div id="product-list"></div>
<button id="btn-load">โหลดสินค้า</button>

<script src="js/api.js"></script>
<script src="js/app.js"></script>
JavaScript// js/app.js
document.getElementById('btn-load').addEventListener('click', async () => {
    try {
        const products = await ProductAPI.getAll();
        const container = document.getElementById('product-list');

        container.innerHTML = products.map(p => `
            <div class="product-card">
                <h3>${p.name}</h3>
                <p>฿${p.price.toLocaleString()}</p>
                <button onclick="deleteProduct(${p.id})">ลบ</button>
            </div>
        `).join('');

    } catch (err) {
        alert(`เกิดข้อผิดพลาด: ${err.message}`);
    }
});

async function deleteProduct(id) {
    if (!confirm('ยืนยันการลบ?')) return;
    await ProductAPI.delete(id);
    document.getElementById('btn-load').click(); // Reload list
}

🔐 Part 3: JWT Authentication

3.1 Login Flow

Flow การ Login แบบ JWT มีขั้นตอนดังนี้:

  1. User กรอก username/password แล้วกด Submit
  2. JS ส่ง POST ไปที่ /api/auth/login
  3. Spring Boot ตรวจสอบ แล้วคืน JWT Token
  4. JS เก็บ Token ใน localStorage
  5. ทุก Request ถัดไปแนบ Token ใน Authorization: Bearer ... Header
JavaScript// js/auth.js
async function login(username, password) {
    const response = await fetch('http://localhost:8080/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    if (!response.ok) throw new Error('Login failed');

    const data = await response.json();
    localStorage.setItem('jwt_token', data.token);
    window.location.href = '/dashboard.html';
}

function logout() {
    localStorage.removeItem('jwt_token');
    window.location.href = '/login.html';
}

// ตรวจสอบสิทธิ์ก่อนโหลดหน้า — เรียกบนทุกหน้าที่ต้อง Login
function requireAuth() {
    if (!localStorage.getItem('jwt_token')) {
        window.location.href = '/login.html';
    }
}
📌 Security Note: localStorage ไม่ปลอดภัยสำหรับ Token ที่ Sensitive มาก — Production ควรพิจารณาใช้ HttpOnly Cookie แทน เพื่อป้องกัน XSS Attack

🐛 Part 4: ปัญหาที่พบบ่อย

ปัญหาที่ 1: CORS Error ยังติดอยู่แม้ตั้งค่าแล้ว

สาเหตุที่พบบ่อย: Spring Security Intercept ก่อนที่ CORS Config จะทำงาน แก้ด้วยการเพิ่มใน SecurityConfig:

Java@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .csrf(csrf -> csrf.disable())
        // ... rest of config
    return http.build();
}

ปัญหาที่ 2: OPTIONS Request ถูก Block

Browser ส่ง Preflight Request (OPTIONS) ก่อนทุก Cross-Origin Request ต้องให้ Spring ผ่าน OPTIONS โดยไม่ต้อง Auth:

Java.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
    .requestMatchers("/api/auth/**").permitAll()
    .anyRequest().authenticated()
)

ปัญหาที่ 3: JSON Parse Error

ตรวจสอบว่า Controller ส่ง Content-Type: application/json กลับมาด้วย ถ้าใช้ @RestController จะทำให้อัตโนมัติแล้ว แต่ถ้าใช้ @Controller ต้องเพิ่ม @ResponseBody หรือใช้ ResponseEntity

✅ เคล็ดลับ: เปิด Browser DevTools → Network Tab → ดู Request/Response Headers ก่อนเสมอ จะเห็นสาเหตุของ Error ได้ชัดเจน

✅ Checklist ก่อน Deploy

รายการสถานะ
CORS ตั้งค่า allowedOrigins ถูก Domainตรวจสอบ
ไม่มี CORS * กับ allowCredentials(true)ตรวจสอบ
OPTIONS Request ผ่านได้โดยไม่ต้อง Authตรวจสอบ
JWT Secret ยาวพอ (256 bit ขึ้นไป)ตรวจสอบ
Error Handling ใน JS ครบทุก Caseตรวจสอบ
ไม่ Expose Sensitive Data ใน Responseตรวจสอบ
HTTPS ใน Productionตรวจสอบ

Vanilla JS + Spring Boot เป็น Stack ที่ Lightweight และเข้าใจง่าย เหมาะสำหรับระบบขนาดกลางที่ไม่ต้องการ Complexity ของ Frontend Framework เพียงแค่เข้าใจ CORS, Fetch API และ JWT ก็พร้อม Deploy ได้เลย

#SpringBoot#Java#VanillaJS#REST API#CORS#JWT#WebDev






PHP CI MANIA - PHP Code Generator 

โปรแกรมช่วยสร้างโค้ด "ลดเวลาการเขียนโปรแกรม"
ราคาสุดคุ้ม  
http://www.phpcodemania.com