Endpoints & API Routes
Route ที่ return ข้อมูล ไม่ใช่หน้าเว็บ
หัวข้อที่มีชื่อว่า “Route ที่ return ข้อมูล ไม่ใช่หน้าเว็บ”ไม่ใช่ทุก route ที่ render HTML ไฟล์ใน src/pages/ ที่มีนามสกุล .ts (หรือ .js) คือ API endpoint: แทนที่จะเป็น template มัน export function ที่ตั้งชื่อตาม HTTP method (GET, POST, PUT, DELETE) แต่ละตัว return Response มาตรฐาน
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = () => { return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' }, });};GET /api/health ตอนนี้ return JSON แล้ว พิสูจน์กับ build ที่รันจริง:
$ curl -s http://127.0.0.1:4351/api/health{"ok":true}type APIRoute ให้ context argument ที่ typed และ return Response — shape มาตรฐานเดียวกับที่ใช้ทั่วทั้ง Astro
อ่าน params และ body
หัวข้อที่มีชื่อว่า “อ่าน params และ body”handler รับ context ที่มี request, params, cookies และอื่น ๆ:
export const prerender = false; // dynamic + POST needs on-demand (or add getStaticPaths for GET-only)import type { APIRoute } from 'astro';
// Stand-in for your real database client — replace with your ORM/query layer.const db = { users: { find: async (id: string | undefined) => ({ id, name: 'Ava' }), create: async (data: any) => ({ id: crypto.randomUUID(), ...data }) } };
export const GET: APIRoute = async ({ params }) => { const user = await db.users.find(params.id); // [id] from the path if (!user) return new Response('Not found', { status: 404 }); return Response.json(user); // shorthand JSON response};
export const POST: APIRoute = async ({ request }) => { const body = await request.json(); // parse the request body const created = await db.users.create(body); return Response.json(created, { status: 201 });};flowchart LR req["GET or POST /api/..."] --> handler["exported handler (GET / POST / ...)"] handler --> read["read params, request body, cookies"] read --> resp["return a Response (JSON, file, status)"]
Static vs on-demand endpoint
หัวข้อที่มีชื่อว่า “Static vs on-demand endpoint”เหมือน page endpoint ก็ build ครั้งเดียวหรือรันต่อ request ได้:
- Static — ด้วย
output: 'static'endpointGETรันตอน build time และ body ของResponseถูกเขียนลงไฟล์ เหมาะกับการ generaterss.xml, sitemap หรือ static JSON API ที่ไม่เปลี่ยนต่อ request (static endpoint รองรับแค่GET) - On-demand — เพิ่ม
export const prerender = false(และ adapter) เพื่อให้ endpoint รันต่อ request เปิดใช้POST/PUT/DELETE, request body และ live data
// A dynamic endpoint that handles form posts:export const prerender = false;Streaming response
หัวข้อที่มีชื่อว่า “Streaming response”เพราะ handler return Response จริง มัน stream ได้แทนที่จะ buffer body ทั้งก้อนไว้ใน memory — สร้างจาก ReadableStream แล้ว Astro (ผ่าน adapter) ส่งต่อเป็น chunked output ให้:
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = () => { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { for (const chunk of ['one\n', 'two\n', 'three\n']) { controller.enqueue(encoder.encode(chunk)); await new Promise((r) => setTimeout(r, 50)); } controller.close(); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/plain' } });};พิสูจน์กับ build ที่รันจริง — ไม่มี Content-Length และ server เจรจา Transfer-Encoding: chunked เอง เหมือน streamed web response ทั่วไป:
$ curl -sD - -o /dev/null http://127.0.0.1:4351/api/stream | grep -i "transfer-encoding\|^HTTP"HTTP/1.1 200 OKTransfer-Encoding: chunkedcontext.clientAddress: on-demand เท่านั้น
หัวข้อที่มีชื่อว่า “context.clientAddress: on-demand เท่านั้น”context.clientAddress อ่าน IP ของ client ที่ request มาจาก adapter — แปลว่ามันมีอยู่ก็ต่อเมื่อมีrequest จริงให้อ่านค่า เรียกมันบน route ที่ prerender (ไม่ว่าจะมีหรือไม่มี adapter config) จะเจอ build failure จริงทันที:
---// @expect-error PrerenderClientAddressNotAvailable — Astro.clientAddress read on a prerendered route (src/pages/ip.astro)export const prerender = true;const ip = Astro.clientAddress;---[ERROR] PrerenderClientAddressNotAvailable: `Astro.clientAddress` cannot be used inside prerendered route src/pages/ip.astro. Error reference: https://docs.astro.build/en/reference/errors/prerender-client-address-not-available/พิสูจน์สองครั้งเหมือนกันเป๊ะ ไม่ว่าโปรเจกต์จะเป็น output: 'static' แบบไม่มี adapter เลย หรือ output: 'server' ที่มี Node adapter ติดตั้งอยู่แต่ route นั้นตัวเดียว mark prerender = true — ทั้งสองแบบไม่มี per-request connection ให้ clientAddress มาจากเลย เพราะหน้านั้นถูก render ครั้งเดียวตอน build ย้าย route ไปเป็น on-demand (เอา prerender = true ออก คง adapter ไว้) แล้วการอ่านแบบเดียวกันก็ทำงาน
Endpoint vs Action
หัวข้อที่มีชื่อว่า “Endpoint vs Action”ทั้งคู่รัน server code แต่แก้ปัญหาคนละแบบ:
- endpoint คือ raw HTTP route ที่คุณออกแบบเอง — คุณเป็นเจ้าของ URL, method, status code และรูปแบบ response หยิบมันมาใช้กับ webhook, third-party integration, file download หรือ public REST/JSON API
- Action (บทถัดไป) คือ type-safe function ที่คุณเรียกจาก client code ของตัวเองพร้อม input validation อัตโนมัติ — ไม่ต้องมี URL หรือจัดการ
Responseเอง หยิบมันมาใช้กับ mutation และ form ของแอปตัวเอง
กฎง่าย ๆ: สัญญาสาธารณะ/ภายนอก → endpoint; app logic ภายในที่เรียกจาก frontend ตัวเอง → Action
เหนือกว่า endpoint: src/fetch.ts (Astro 7)
หัวข้อที่มีชื่อว่า “เหนือกว่า endpoint: src/fetch.ts (Astro 7)”Endpoint ให้คุณจัดการทีละ route Astro 7 ทำให้ advanced routing เสถียร: ไฟล์สงวนชื่อ src/fetch.ts ที่อยู่หน้า pipeline ของ request ทั้งหมด มัน export fetch handler มาตรฐาน — shape (request) => Response เดียวกับที่ Cloudflare Workers, Deno และ Bun ใช้ — จึงรัน logic ก่อน router ของ Astro ได้, mount แอป Hono พร้อม middleware ของมันได้ หรือเขียน routing เองสำหรับ URL บางส่วน แล้วปล่อยที่เหลือให้ Astro จัดการ
หยิบมันมาใช้เมื่อ middleware ไม่พอ: proxy API เก่าใต้ origin เดียวกัน, ต่อแอป Hono/Web-standard ที่มีอยู่แล้วเข้ากับ Astro site หรือจัด request ในแบบที่ file-based router ทำไม่ได้ สำหรับ JSON route เดียว endpoint ปกติก็ยังเป็นเครื่องมือที่ถูกต้อง
เพราะชื่อไฟล์นี้ถูกสงวนแล้ว src/fetch.ts ที่ไม่เกี่ยวข้องในโปรเจกต์เก่าต้อง rename — หรือชี้ Astro ไปที่อื่น:
import { defineConfig } from 'astro/config';import node from '@astrojs/node';
export default defineConfig({ adapter: node({ mode: 'standalone' }), fetchFile: './src/router.ts', // or null to disable advanced routing});อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ review endpoint ที่ generate มา ให้เช็ค:
- endpoint ที่ dynamic หรือจัดการ
POSTsetexport const prerender = falseจริงไหม? ถ้าไม่ (และอยู่ใต้output: 'static') จะมีแค่GETที่ทำงาน และ response จะแข็งตัวตอน build — handlerPOSTที่ไม่เคยเข้าถึงได้จริงเป็นความผิดพลาดที่ AI generate บ่อย - handler อ่าน
context.clientAddress(rate limiting, geolocation, audit log) บน route ที่ prerender หรือเปล่า? พิสูจน์แล้วข้างบน: นั่นคือ build failure จริง ไม่ใช่undefinedตอน runtime - response ที่ใหญ่หรือสร้างช้าถูก buffer เป็น string ก้อนเดียวไหม ทั้งที่
ReadableStream(พิสูจน์แล้วข้างบนด้วย chunked output จริง) จะเริ่มส่ง byte ได้ทันที? - handler สมมติว่า
request.json()สำเร็จเสมอ โดยไม่มีtry/catchรอบ input ที่ผิดรูปแบบหรือเปล่า? - business logic ที่จริง ๆ คือ “แอปของฉันเรียก backend ของตัวเอง” ถูก implement เป็น endpoint พร้อม
fetch()ที่เขียนเองบน client ทั้งที่ Action (บทถัดไป) ให้ผลเดียวกันพร้อม validation อัตโนมัติโดยไม่ต้องดูแล URL?
🐛 หา bug ในโค้ดนี้
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ clientAddress }) => {await logAccess(clientAddress);return Response.json({ ok: true });};เฉลย
ที่นี่ไม่มี export const prerender = false เลย และไม่มีอะไรรับประกันว่า route นี้จะมี adapter config ไว้ด้วย — ถ้าโปรเจกต์นี้ (หรือแค่ route นี้) จบเป็น static clientAddress จะถูกอ่านบน route ที่ไม่มี request จริงอยู่เบื้องหลัง ความล้มเหลวจริงที่พิสูจน์แล้วจากสถานการณ์แบบนี้เป๊ะคือ build ล่มตอน build-time ไม่ใช่ค่าที่เป็น undefined เงียบ ๆ: PrerenderClientAddressNotAvailable: Astro.clientAddress cannot be used inside prerendered route ... วิธีแก้คือทำให้ intent ว่า on-demand ชัดเจน — export const prerender = false — และให้แน่ใจว่าโปรเจกต์มี adapter config ไว้ เพื่อให้ route นี้มี request จริงให้อ่าน client address ได้