ข้ามไปยังเนื้อหา

Endpoints & API Routes

ไม่ใช่ทุก route ที่ render HTML ไฟล์ใน src/pages/ ที่มีนามสกุล .ts (หรือ .js) คือ API endpoint: แทนที่จะเป็น template มัน export function ที่ตั้งชื่อตาม HTTP method (GET, POST, PUT, DELETE) แต่ละตัว return Response มาตรฐาน

src/pages/api/health.ts
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

handler รับ context ที่มี request, params, cookies และอื่น ๆ:

src/pages/api/users/[id].ts
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)"]
Endpoint map method ไปยัง Response

เหมือน page endpoint ก็ build ครั้งเดียวหรือรันต่อ request ได้:

  • Static — ด้วย output: 'static' endpoint GET รันตอน build time และ body ของ Response ถูกเขียนลงไฟล์ เหมาะกับการ generate rss.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;

เพราะ handler return Response จริง มัน stream ได้แทนที่จะ buffer body ทั้งก้อนไว้ใน memory — สร้างจาก ReadableStream แล้ว Astro (ผ่าน adapter) ส่งต่อเป็น chunked output ให้:

src/pages/api/stream.ts
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 OK
Transfer-Encoding: chunked

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 ไว้) แล้วการอ่านแบบเดียวกันก็ทำงาน

ทั้งคู่รัน 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 ให้คุณจัดการทีละ 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 ไปที่อื่น:

astro.config.mjs
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
});

เมื่อ review endpoint ที่ generate มา ให้เช็ค:

  • endpoint ที่ dynamic หรือจัดการ POST set export const prerender = false จริงไหม? ถ้าไม่ (และอยู่ใต้ output: 'static') จะมีแค่ GET ที่ทำงาน และ response จะแข็งตัวตอน build — handler POST ที่ไม่เคยเข้าถึงได้จริงเป็นความผิดพลาดที่ 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 ในโค้ดนี้

src/pages/api/report.ts
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 ได้

define API endpoint ใน Astro ยังไง?
static (build-time) endpoint รองรับ method อะไรบ้าง?
นี่พิสูจน์อะไรเกี่ยวกับ endpoint ที่สร้างมัน?
$ curl -sD - -o /dev/null http://127.0.0.1:4351/api/stream | grep -i "transfer-encoding"
Transfer-Encoding: chunked
route มี export const prerender = true (หรือโปรเจกต์ไม่มี adapter เลย) และ handler อ่าน context.clientAddress เกิดอะไรขึ้นจริง ตามที่พิสูจน์ในบทนี้?
ควรใช้ Action แทน endpoint เมื่อไร?
endpoint handler return อะไร?