Day 4
เชื่อมหน้าบ้านเข้ากับ API จริง
เวลา: 120 นาที จบคาบนี้จะได้: หน้าเว็บดึงข้อมูลจากฐานข้อมูลจริงผ่าน API พร้อมช่องค้นหา และจัดการสถานะกำลังโหลด / ผิดพลาด / ไม่พบข้อมูล ครบ
ช่วงที่ 1 — เข้าใจการคุยกันสองฝั่ง (25 นาที)
🗺️ ตำแหน่งตอนนี้ในระบบ
- ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
- 🟢 Angular (หน้าบ้าน) ← กำลังทำช่วงนี้
- 🟢 NestJS (หลังบ้าน) ← กำลังทำช่วงนี้
- ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)
ต่อไป: สร้างตัวกลาง (service) ที่ยิง HTTP ไปหาหลังบ้าน

1.1 รันทั้งสองฝั่ง
Terminal ที่ 1
cd C:\webdev\booklab
npm run dev:api
Terminal ที่ 2
cd C:\webdev\booklab
npm run dev:web
1.2 ทดลองเห็นปัญหาก่อนแก้
เปิด localhost:4200 แล้วกด F12 เปิดเครื่องมือนักพัฒนา → เลือกแท็บ Network
💡 แท็บ Network คือเครื่องมือ debug ที่สำคัญที่สุดของงานฝั่งหน้าบ้าน มันแสดงทุกคำขอที่หน้าเว็บส่งออกไป พร้อมคำตอบที่ได้กลับมา ตั้งแต่คาบนี้ไป เวลาหน้าเว็บว่าง ให้เปิดตรงนี้ดูก่อนเสมอ ว่า
- ส่งคำขอออกไปหรือเปล่า
- ส่งไปที่ URL ถูกไหม
- ได้อะไรกลับมา
พิมพ์ http://localhost:3000/api/books ในเบราว์เซอร์
✅ ต้องได้ข้อมูลหนังสือ JSON กลับมาจาก API ฝั่งหลังบ้าน (port 3000)
💡 เข้าใจการทำงานของ CORS หน้าบ้าน Angular (port 4200) และหลังบ้าน NestJS (port 3000) รันอยู่คนละ port กัน ปกติเบราว์เซอร์จะมีกฎความปลอดภัยชื่อ CORS (Cross-Origin Resource Sharing) บล็อกไม่ให้ดึงข้อมูลข้าม port การที่เราใส่
app.enableCors()ไว้ที่ไฟล์main.tsของ NestJS ตั้งแต่คาบที่ 1 ทำให้ Angular สามารถยิง HTTP มาที่http://localhost:3000/api/booksได้โดยตรงอย่างปลอดภัย
1.3 เปิดใช้ HttpClient
แก้ apps/web/src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
],
};
💡
HttpClient= เครื่องมือของ Angular สำหรับส่งคำขอไปยัง API เปรียบเหมือนรถที่ใช้วิ่งไปกลับ 💡provideHttpClient()= เปิดใช้งาน ต้องประกาศไว้ที่นี่ครั้งเดียว แล้วทุก component ใช้ได้หมด
ช่วงที่ 2 — สร้างคนวิ่งส่งของ (25 นาที)
🗺️ ตำแหน่งตอนนี้ในระบบ
- ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
- 🟢 Angular (หน้าบ้าน) ← กำลังทำช่วงนี้
- ✅ NestJS (หลังบ้าน) (ทำไปแล้ว)
- ✅ PostgreSQL (ฐานข้อมูล) (ทำไปแล้ว)
ต่อไป: ใช้ service ที่สร้างไว้ดึงข้อมูลจริงมาแสดง
2.1 สร้าง service
cd apps\web
npm exec -- ng generate service books/book --injectable --skip-tests --type=service
⚠️ ต้องใส่ flag
--injectable --type=serviceเสมอ Angular เวอร์ชันนี้เปลี่ยนค่าเริ่มต้นของคำสั่งนี้ไปสร้างโค้ดแบบใหม่ที่ชื่อ@Service()แทน@Injectable()ที่เราใช้กันทั้งวิชา ถ้าลืมใส่ทั้งสอง flag นี้ โค้ดที่ได้จะไม่ตรงกับที่เรียนมาและใช้งานไม่ได้ จำไว้ว่าเวลาสร้าง service ใน Angular ต้องพิมพ์ยาวแบบนี้เสมอ
✅ ต้องเห็น ไฟล์ apps/web/src/app/books/book.service.ts เกิดขึ้น เปิดดูจะเป็นโครงเปล่า ๆ แบบนี้
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class BookService {}
แก้ไฟล์นี้ให้เป็นแบบนี้
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { Book } from './book';
@Injectable({ providedIn: 'root' })
export class BookService {
private http = inject(HttpClient);
private apiUrl = 'http://localhost:3000/api/books';
getAll(q: string = ''): Promise<Book[]> {
return firstValueFrom(this.http.get<Book[]>(this.apiUrl, { params: { q } }));
}
getById(id: number): Promise<Book> {
return firstValueFrom(this.http.get<Book>(`${this.apiUrl}/${id}`));
}
}
💡 service ฝั่ง Angular คือคนวิ่งส่งของ หน้าเว็บอยากได้ข้อมูล ก็ให้คนนี้วิ่งไปติดต่อ API แทน component ทุกตัวเรียกใช้คนเดียวกันได้ ไม่ต้องเขียนโค้ดยิง API ซ้ำในทุกหน้า
💡
@Injectable({ providedIn: 'root' })= สร้างไว้ตัวเดียวใช้ร่วมกันทั้งแอป 💡inject(HttpClient)= ขอ HttpClient มาใช้ 💡private apiUrl = 'http://localhost:3000/api/books';= URL ของ API ฝั่งหลังบ้าน💡
this.http.get<Book[]>(...)ตัว<Book[]>ในวงเล็บมุมคือการบอกว่า "ข้อมูลที่จะได้กลับมาเป็นรายการหนังสือ" ทำให้ VS Code ช่วยเตือนได้ถ้าเราเผลอใช้ผิดช่อง💡
firstValueFrom(...)— ทำไมต้องมี HttpClient คืนค่าเป็นสิ่งที่เรียกว่า Observable ซึ่งใช้งานคนละแบบกับที่เราคุ้น เราจึงห่อด้วยfirstValueFromเพื่อแปลงเป็น Promise ที่ใช้awaitได้ตามที่เรียนมาในคาบแรก💡
${this.apiUrl}/${id}= ต่อ URL เพื่อระบุ id ของหนังสือที่ต้องการ
⚠️ ยังต้องมีไฟล์
book.ts(interface) ตามที่สร้างไว้แล้วในคาบที่ 3 ข้อ 2.2 ถ้ายังไม่มีให้กลับไปสร้างก่อน เพราะ service ตัวนี้ import จากไฟล์นั้น
ช่วงที่ 3 — ดึงข้อมูลมาแสดง (35 นาที)
🗺️ ตำแหน่งตอนนี้ในระบบ
- ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
- 🟢 Angular (หน้าบ้าน) ← กำลังทำช่วงนี้
- 🟢 NestJS (หลังบ้าน) ← กำลังทำช่วงนี้
- 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้
ต่อไป: เพิ่มช่องค้นหาที่ส่งคำค้นไปกรองข้อมูลถึงฐานข้อมูล
3.1 เปลี่ยนจากข้อมูลจำลองเป็นของจริง
แทนที่ apps/web/src/app/books/book-list/book-list.ts
import { Component, signal, inject, OnInit } from '@angular/core';
import { BookService } from '../book.service';
import { BookCard } from '../book-card/book-card';
import { Book } from '../book';
@Component({
selector: 'app-book-list',
imports: [BookCard],
templateUrl: './book-list.html',
styleUrl: './book-list.css',
})
export class BookList implements OnInit {
private bookService = inject(BookService);
books = signal<Book[]>([]);
loading = signal(false);
error = signal('');
ngOnInit() {
this.load();
}
async load() {
this.loading.set(true);
this.error.set('');
try {
const result = await this.bookService.getAll();
this.books.set(result);
} catch {
this.error.set('โหลดข้อมูลไม่สำเร็จ กรุณาตรวจว่า API ทำงานอยู่');
} finally {
this.loading.set(false);
}
}
}
💡
ngOnInit()= ฟังก์ชันที่ Angular เรียกให้อัตโนมัติตอน component ถูกสร้างเสร็จ เป็นจังหวะที่เหมาะกับการโหลดข้อมูลครั้งแรก💡
try / catch / finally= ดักความผิดพลาด
try= ลองทำcatch= ถ้าพัง ให้ทำอะไรfinally= ทำเสมอไม่ว่าจะสำเร็จหรือพัง — เราใช้ปิดสถานะกำลังโหลด💡 ทำไมต้องมีสามสถานะ เวลาไปขอข้อมูลจากที่อื่น มีความเป็นไปได้สามอย่างเสมอ คือ กำลังรอ, ได้ของ, พัง ถ้าไม่จัดการให้ครบ ผู้ใช้จะเจอหน้าขาว ๆ แล้วไม่รู้ว่าเกิดอะไรขึ้น — นี่คือสิ่งที่แยกเว็บที่ทำเสร็จ กับเว็บที่ทำเสร็จจริง
3.2 แสดงทั้งสามสถานะ
แทนที่ book-list.html
<div class="p-6 max-w-3xl mx-auto">
<h1 class="text-2xl font-bold mb-4">รายการหนังสือ</h1>
@if (loading()) {
<p class="text-gray-500">กำลังโหลด...</p>
} @else if (error()) {
<div class="border border-red-300 bg-red-50 text-red-700 rounded p-4">
{{ error() }}
</div>
} @else if (books().length === 0) {
<p class="text-gray-500">ไม่พบข้อมูล</p>
} @else {
<div class="grid gap-3">
@for (b of books(); track b.id) {
<app-book-card [book]="b" />
}
</div>
}
</div>
💡
@if / @else if / @else= เลือกแสดงอย่างใดอย่างหนึ่ง ลำดับสำคัญ Angular จะไล่จากบนลงล่างแล้วหยุดที่อันแรกที่เป็นจริง
✅ เปิด localhost:4200 ต้องเห็นหนังสือ 5 เล่มจากฐานข้อมูลจริง พร้อมป้ายหมวดหมู่
3.3 ทดลองทำให้พัง — สำคัญมาก
ไปที่ Terminal ที่รัน API แล้วกด Ctrl + C เพื่อปิด จากนั้นกด refresh หน้าเว็บ
✅ ต้องเห็นกล่องสีแดง ไม่ใช่หน้าขาว
เปิด F12 → Network จะเห็นคำขอที่ล้มเหลวเป็นสีแดง
แล้วเปิด API กลับมา (npm run dev:api) กด refresh อีกครั้ง ต้องกลับมาปกติ
💡 นี่คือขั้นตอน debug ที่จะใช้ตลอด — หน้าเว็บว่างเมื่อไหร่ ให้ถามสามคำถามตามลำดับ
- API รันอยู่ไหม (ดู Terminal)
- มีข้อมูลในฐานข้อมูลไหม (เปิด Prisma Studio)
- คำขอส่งไปถูก URL ไหม ได้อะไรกลับมา (ดู F12 → Network)
ช่วงที่ 4 — เพิ่มช่องค้นหา (25 นาที)
🗺️ ตำแหน่งตอนนี้ในระบบ
- ✅ เตรียมเครื่องมือ (ทำไปแล้ว)
- 🟢 Angular (หน้าบ้าน) ← กำลังทำช่วงนี้
- 🟢 NestJS (หลังบ้าน) ← กำลังทำช่วงนี้
- 🟢 PostgreSQL (ฐานข้อมูล) ← กำลังทำช่วงนี้
ต่อไป: คาบหน้า: ทำหน้ารายละเอียดและเพิ่ม-แก้-ลบข้อมูล
4.1 เพิ่มความสามารถค้นหา
แก้ book-list.ts เพิ่มตัวแปรและแก้ load()
export class BookList implements OnInit {
private bookService = inject(BookService);
books = signal<Book[]>([]);
loading = signal(false);
error = signal('');
keyword = signal('');
ngOnInit() {
this.load();
}
async load() {
this.loading.set(true);
this.error.set('');
try {
const result = await this.bookService.getAll(this.keyword());
this.books.set(result);
} catch {
this.error.set('โหลดข้อมูลไม่สำเร็จ กรุณาตรวจว่า API ทำงานอยู่');
} finally {
this.loading.set(false);
}
}
onSearch(value: string) {
this.keyword.set(value);
this.load();
}
}
4.2 เพิ่มช่องกรอกในหน้าจอ
แทรกก่อนบล็อก @if ใน book-list.html
<div class="flex gap-2 mb-4">
<input
#searchBox
(keyup.enter)="onSearch(searchBox.value)"
placeholder="ค้นหาชื่อหนังสือ"
class="border rounded px-3 py-2 flex-1" />
<button
(click)="onSearch(searchBox.value)"
class="px-4 py-2 bg-blue-600 text-white rounded">
ค้นหา
</button>
</div>
💡
#searchBox= ตั้งชื่อเล่นให้ช่องกรอกนี้ เพื่อให้อ้างถึงได้ใน HTML เดียวกัน เขียนsearchBox.valueก็ได้ค่าที่ผู้ใช้พิมพ์💡
(keyup.enter)= เมื่อกดปุ่ม Enter ในช่องนี้ให้ทำสิ่งนี้ — Angular ดักปุ่มเฉพาะได้เลย ไม่ต้องเช็ครหัสปุ่มเอง
✅ พิมพ์ clean แล้วกด Enter ต้องเหลือแค่ Clean Code
✅ พิมพ์คำที่ไม่มีจริง ต้องขึ้น "ไม่พบข้อมูล" ไม่ใช่หน้าว่าง
4.3 ดูว่าเกิดอะไรขึ้นเบื้องหลัง
เปิด F12 → Network → พิมพ์คำค้นแล้วกด Enter
✅ ต้องเห็นคำขอไปที่ /api/books?q=clean
💡
?q=cleanคือ query parameter — วิธีส่งค่าเพิ่มเติมไปกับ URL ฝั่ง API รับด้วย@Query('q')ที่เราเขียนไว้ในคาบที่ 2 แล้วส่งต่อให้ Prisma ใช้เป็นเงื่อนไขcontainsลองไล่ดูเส้นทางทั้งหมด: ช่องกรอก →
onSearch()→BookService→ HTTP (CORS) → NestJS controller → service → Prisma → PostgreSQL แล้ววิ่งกลับทางเดิม — นี่คือภาพรวมทั้งระบบที่เราสร้างมาสี่คาบ
🚀 ถ้าเสร็จก่อนเพื่อน
- เพิ่มตัวเลขบอกว่าพบกี่เล่ม
- ทำให้ปุ่มค้นหากดไม่ได้ระหว่างกำลังโหลด (คำใบ้:
[disabled]="loading()") - เพิ่มปุ่มล้างคำค้นให้กลับมาแสดงทั้งหมด
❓ ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ขึ้นกล่องแดงตลอด | API ไม่ได้รัน | เปิด Terminal รัน npm run dev:api |
404 ใน Network |
URL ผิด | ต้องเป็น http://localhost:3000/api/books |
NullInjectorError: No provider for HttpClient |
ลืมใส่ใน app.config.ts |
เพิ่ม provideHttpClient() |
| ค้นหาแล้วไม่กรอง | API ไม่ได้รับค่า | ดู Network ว่ามี ?q= ต่อท้ายไหม |
| หน้าว่างไม่มี error | ฐานข้อมูลไม่มีข้อมูล | เปิด npm exec -- prisma studio ตรวจ แล้ว seed ใหม่ |
📌 สรุปเส้นทางข้อมูลทั้งระบบ
ผู้ใช้พิมพ์
↓
Component (book-list.ts)
↓
Service (book.service.ts) → HttpClient
↓
HTTP Request (CORS: 4200 → 3000)
↓
NestJS Controller (@Query)
↓
NestJS Service → Prisma
↓
PostgreSQL
↓
วิ่งกลับทางเดิมมาแสดงบนหน้าจอ
คาบหน้า ทำหน้ารายละเอียด และเพิ่ม–แก้–ลบ ข้อมูลได้