Skip to content
Cloud AI エンジニア入門ガイド
Go back

10. AstroのエンドポイントとActions

この章の全体像として、エンドポイント、Actions、フォーム対応、認可の4つを番号順に並べ、データの受け渡しを安全に書けるようになることを示した図

シリーズ目次前章: 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,
  });
};

GETPOSTDELETE、そしてすべてを受ける ALL をエクスポートできます。エクスポートしていないメソッドでアクセスされた場合は、404ページが返ります。

2種類のエンドポイントを並べると、次のようになります。

ビルド時に内容が決まる静的ファイルエンドポイントはファイルとして出力され配信されるのに対し、prerenderをfalseにしたサーバーエンドポイントはリクエストのたびに実行されアダプターを必要とすることを比較した図

Actions

エンドポイントは自由度が高いぶん、毎回同じ処理を書くはめになります。リクエストボディのJSON解析、入力値の検証、エラー時のレスポンス整形。この定型作業を引き受けてくれるのがActionsです。

Actionsは、サーバー側の関数を、型安全にクライアントから呼び出せるようにする仕組みです。

定義する

src/actions/index.tsserver オブジェクトをエクスポートします。

// 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}さん`;
    },
  }),
};

前章までで登場したZodの記法が、ここでも再び使えます。

呼び出す

クライアント側からは、astro:actionsactions オブジェクト経由で呼び出します。

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サーバーがActionを呼び出し、入力検証とデータ処理を経て結果をHTMLとして返すまでの流れを示したシーケンス図

結果を受け取る

フォーム送信の結果は、ページ側で 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 に入っている情報をもとに判定するのが基本形です。

画面のボタンからの呼び出しも外部からの直接の呼び出しも同じActionに届くため、ボタンを隠すことは認可にならず、ハンドラーの中で権限を確認して必要ならActionErrorを返す必要があることを示した図

エンドポイントとActionsの使い分け

状況選択
自サイトのフォーム送信を処理したいActions
自サイトのJavaScriptからサーバー処理を呼びたいActions
外部サービスやアプリに公開するAPIを作りたいエンドポイント
RSSやJSONなど、決まった形式のファイルを出力したいエンドポイント
Webhookを受け取りたいエンドポイント

サイト内部の処理はActions、外部との境界はエンドポイント。だいたいこれで済みます。

自サイトのフォーム送信や自サイトのJavaScriptからの呼び出しはActionsで受け、外部サービスへのAPI公開やRSS出力やWebhookの受信はエンドポイントで受けるという役割分担を、二つのレーンで示した図

要点

参考資料


シリーズ目次前章: 09. Astroのレンダリングモードとアダプター次章: 11. Astroの画像最適化とView Transitions


Share this post:

Previous Post
11. Astroの画像最適化とView Transitions
Next Post
09. Astroのレンダリングモードとアダプター