シリーズ目次 / 前章: 09. Astroのレンダリングモードとアダプター / 次章: 11. Astroの画像最適化とView Transitions
Table of contents
Open Table of contents
この章のねらい
ここまで作ってきたのは、HTMLを返すページでした。ただサイトには、HTML以外を返したい場所もあります。RSSフィード、JSONのデータ、フォームの送信先。
Astroにはそのための仕組みが2つあります。エンドポイントとActionsです。役割と使い分けを見ます。
エンドポイント
エンドポイントは、HTML以外のレスポンスを返すためのファイルです。src/pages/ に .js または .ts ファイルを置くと作れます。
ページと同じくファイルベースルーティングに従い、拡張子を含めたファイル名がURLになります。
| ファイル | URL |
|---|---|
src/pages/data.json.ts | /data.json |
src/pages/feed.xml.ts | /feed.xml |
src/pages/api/posts.ts | /api/posts |
静的ファイルエンドポイント
ビルド時に内容が決まるデータは、静的ファイルとして出力できます。
// src/pages/data.json.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = () => {
return new Response(
JSON.stringify({
name: "サンプルサイト",
updated: "2026-08-02",
}),
{
headers: { "Content-Type": "application/json" },
}
);
};
HTTPメソッド名(GET)をエクスポートするのが決まりです。返すのはWeb標準の Response オブジェクトなので、他の環境で覚えたことがそのまま通用します。
コンテンツコレクションと組み合わせれば、記事一覧のJSONを生成できます。
// src/pages/posts.json.ts
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
export const GET: APIRoute = async () => {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const body = posts.map(post => ({
id: post.id,
title: post.data.title,
pubDate: post.data.pubDate,
}));
return new Response(JSON.stringify(body), {
headers: { "Content-Type": "application/json" },
});
};
動的ルートも使えます。その場合はページと同様に getStaticPaths() を用意します。
// src/pages/api/[id].json.ts
export function getStaticPaths() {
return [{ params: { id: "1" } }, { params: { id: "2" } }];
}
export const GET: APIRoute = ({ params }) => {
return new Response(JSON.stringify({ id: params.id }), {
headers: { "Content-Type": "application/json" },
});
};
サーバーエンドポイント
リクエストのたびに処理を行いたい場合は、prerender = false を宣言します(サイト全体が output: "server" なら不要です)。この場合、アダプターが必要になります。
// src/pages/api/subscribe.ts
import type { APIRoute } from "astro";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
if (!data.email) {
return new Response(JSON.stringify({ message: "メールアドレスが必要です" }), {
status: 400,
});
}
// ここで登録処理を行う
return new Response(JSON.stringify({ message: "登録しました" }), {
status: 200,
});
};
GET、POST、DELETE、そしてすべてを受ける ALL をエクスポートできます。エクスポートしていないメソッドでアクセスされた場合は、404ページが返ります。
2種類のエンドポイントを並べると、次のようになります。
Actions
エンドポイントは自由度が高いぶん、毎回同じ処理を書くはめになります。リクエストボディのJSON解析、入力値の検証、エラー時のレスポンス整形。この定型作業を引き受けてくれるのがActionsです。
Actionsは、サーバー側の関数を、型安全にクライアントから呼び出せるようにする仕組みです。
定義する
src/actions/index.ts に server オブジェクトをエクスポートします。
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const server = {
getGreeting: defineAction({
input: z.object({
name: z.string(),
}),
handler: async input => {
return `こんにちは、${input.name}さん`;
},
}),
};
input: 受け取る値の形をZodで宣言します。検証はAstroが自動で行いますhandler: 検証を通過した値を受け取って処理を行います
前章までで登場したZodの記法が、ここでも再び使えます。
呼び出す
クライアント側からは、astro:actions の actions オブジェクト経由で呼び出します。
import { actions } from "astro:actions";
const { data, error } = await actions.getGreeting({ name: "Houston" });
if (!error) {
console.log(data); // こんにちは、Houstonさん
}
戻り値が { data, error } の形になっているのが特徴です。例外を投げるのではなく値として返されるため、エラー処理の書き忘れが起きにくくなっています。
引数と戻り値には、サーバー側の定義から自動的に型が付きます。APIのURLや型定義を二重に管理する必要がありません。
フォームから使う
accept: "form" を指定すると、HTMLフォームの送信を直接受け取れます。
Actionsを実行するには、デプロイ先に対応したサーバーアダプターが必要です。さらに、HTMLフォームの action 属性からActionを呼び出すページは、オンデマンドレンダリングにする必要があります。アダプターの導入方法は09. Astroのレンダリングモードとアダプターを参照してください。
// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro/zod";
export const server = {
newsletter: defineAction({
accept: "form",
input: z.object({
email: z.string().email(),
terms: z.boolean(),
}),
handler: async ({ email }) => {
// 登録処理
return { registered: email };
},
}),
};
フォーム側は、action 属性にActionを渡すだけです。
---
export const prerender = false;
import { actions } from "astro:actions";
---
<form method="POST" action={actions.newsletter}>
<input type="email" name="email" required />
<label>
<input type="checkbox" name="terms" required />
規約に同意する
</label>
<button>登録する</button>
</form>
JavaScriptが無効な環境でも動作する点が、この書き方の利点です。
処理全体の流れは次のようになります。
結果を受け取る
フォーム送信の結果は、ページ側で Astro.getActionResult() を使って取得します。
---
import { actions } from "astro:actions";
const result = Astro.getActionResult(actions.newsletter);
if (result && !result.error) {
return Astro.redirect("/thanks/");
}
---
{result?.error && <p class="error">登録に失敗しました。入力内容をご確認ください。</p>}
<form method="POST" action={actions.newsletter}>
<!-- 省略 -->
</form>
成功時にリダイレクトし、失敗時はエラーを表示してフォームを再表示する、という典型的な流れを素直に書けます。
エラーを返す
意図的にエラーを返したいときは ActionError を投げます。
import { defineAction, ActionError } from "astro:actions";
export const server = {
likePost: defineAction({
input: z.object({ postId: z.string() }),
handler: async (input, context) => {
if (!context.locals.user) {
throw new ActionError({
code: "UNAUTHORIZED",
message: "ログインが必要です",
});
}
// いいね処理
return { liked: input.postId };
},
}),
};
呼び出し側では、エラーコードで分岐できます。
const { data, error } = await actions.likePost({ postId: "abc" });
if (error?.code === "UNAUTHORIZED") {
window.location.href = "/login/";
}
認可はハンドラー内で行う
Actionは誰でも呼び出せる入口です。ボタンを非表示にしたところで、Action自体は外部から直接叩けます。
ログイン状態や権限の確認は、必ず handler の中で行ってください。上の例のように、context.locals に入っている情報をもとに判定するのが基本形です。
エンドポイントとActionsの使い分け
| 状況 | 選択 |
|---|---|
| 自サイトのフォーム送信を処理したい | Actions |
| 自サイトのJavaScriptからサーバー処理を呼びたい | Actions |
| 外部サービスやアプリに公開するAPIを作りたい | エンドポイント |
| RSSやJSONなど、決まった形式のファイルを出力したい | エンドポイント |
| Webhookを受け取りたい | エンドポイント |
サイト内部の処理はActions、外部との境界はエンドポイント。だいたいこれで済みます。
要点
- エンドポイントは
src/pages/に置く.js/.tsファイルで、Web標準のResponseを返す。 - ビルド時に確定するデータは静的ファイルとして、都度処理が必要なものは
prerender = falseでサーバーエンドポイントとして扱う。 - ActionsはZodによる入力検証と型付けを自動化し、
{ data, error }の形で結果を返す。 accept: "form"を使うと、JavaScriptなしでも動くフォーム処理を書ける。- Actionは外部から直接呼べるため、認可チェックは必ずハンドラー内で行う。
参考資料
シリーズ目次 / 前章: 09. Astroのレンダリングモードとアダプター / 次章: 11. Astroの画像最適化とView Transitions