เชื่อมต่อ Spring Boot API
กับ Vanilla HTML/CSS/JS Frontend
ทำ REST API ให้คุยกับ Frontend แบบ Pure JS ไม่ง้อ Framework — ครอบคลุม CORS, Fetch API และ JWT Authentication
🚀 ทำไมต้อง 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) │ └─────────────────────┘
⚙️ 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);
}
};
}
}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 / UI2.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 มีขั้นตอนดังนี้:
- User กรอก username/password แล้วกด Submit
- JS ส่ง
POSTไปที่/api/auth/login - Spring Boot ตรวจสอบ แล้วคืน JWT Token
- JS เก็บ Token ใน
localStorage - ทุก 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';
}
}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
✅ 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 ได้เลย
