import { Steps } from '@astrojs/starlight/components'; import Since from '/components/Since.astro'; import ReadMore from '/components/ReadMore.astro';

Astroアクションを使うと、型安全なバックエンド関数を定義し、どこからでも呼び出せます。データ取得やJSON解析、入力バリデーションを自動で行うため、APIエンドポイントを使う場合に比べてボイラープレートを大幅に削減できます。

APIエンドポイントの代わりにアクションを使うと、クライアントとサーバーコード間の通信がシームレスになり、次のメリットがあります。

基本的な使い方

アクションはsrc/actions/index.tsserverオブジェクトとしてエクスポートします。

ts
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  myAction: defineAction({ /* ... */ })}

定義したアクションはastro:actionsモジュールから関数として利用できます。UIフレームワークコンポーネント内、フォームのPOSTリクエスト、またはAstroコンポーネント内の<script>タグで呼び出してください。

アクションを呼び出すと、data(JSON直列化された結果)またはerror(スローされたエラー)が入ったオブジェクトが返ります。

astro
------<script>import { actions } from 'astro:actions';async () => {  const { data, error } = await actions.myAction({ /* ... */ });}</script>

最初のアクションを書いてみる

以下の手順でアクションを定義し、Astroページのscriptタグから呼び出します。

  1. src/actions/index.tsを作成し、serverオブジェクトをエクスポートします。

    ts
    export const server = {  // アクションをここに宣言}
  2. defineAction()ユーティリティをastro:actionsから、zオブジェクトをastro:schemaからインポートします。

    ts
    import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  // アクションをここに宣言}
  3. defineAction()getGreetingアクションを定義します。inputプロパティはZodスキーマで入力を検証し、handler()がサーバーで実行されるバックエンドロジックです。

    ts
    import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  getGreeting: defineAction({    input: z.object({      name: z.string(),    }),    handler: async (input) => {      return `Hello, ${input.name}!`    }  })}
  4. ボタンをクリックするとgetGreetingアクションで挨拶を取得するAstroコンポーネントを作成します。

    astro
    ------<button>Get greeting</button><script>const button = document.querySelector('button');button?.addEventListener('click', async () => {  // アクションから取得した挨拶を表示});</script>
  5. actionsastro:actionsからインポートし、クリックハンドラ内でactions.getGreeting()を呼び出します。nameオプションがサーバー側のhandler()に渡され、エラーがなければ結果がdataとして返ります。

    astro
    ------<button>Get greeting</button><script>import { actions } from 'astro:actions';const button = document.querySelector('button');button?.addEventListener('click', async () => { // アクションから挨拶を取得してアラート表示  const { data, error } = await actions.getGreeting({ name: "Houston" });  if (!error) alert(data);})</script>

defineAction()の全プロパティはAPIリファレンスを参照してください。

アクションの整理方法

すべてのアクションはsrc/actions/index.tsserverオブジェクトからエクスポートする必要があります。定義をそのまま書いても、別ファイルへ切り出してインポートしても構いません。関連する関数をネストしてまとめることもできます。

たとえば、ユーザー関連のアクションをまとめる場合、src/actions/user.tsgetUsercreateUserをまとめたuserオブジェクトを作成します。

ts
// src/actions/user.tsimport { defineAction } from 'astro:actions';export const user = {  getUser: defineAction(/* ... */),  createUser: defineAction(/* ... */),}

その後、src/actions/index.tsでインポートし、他のアクションと並べてトップレベルに追加します。

ts
import { user } from './user';export const server = {  myAction: defineAction({ /* ... */ }),  user,}

これでユーザー関連アクションはactions.userから呼び出せます。

  • actions.user.getUser()
  • actions.user.createUser()

返り値の扱い

アクションはhandler()の型安全な戻り値を持つdata、またはバックエンドエラーを持つerrorを返します。errorinputのバリデーションエラーやhandler()内でスローされたエラーです。

アクションはDates、Maps、Sets、URLsを扱える独自フォーマットで返します(Devalueライブラリ使用)。そのため通常のJSONのようにネットワークレスポンスを簡単に検査できません。デバッグ時はアクションが返すdataオブジェクトを確認してください。

handler()のAPIリファレンスを参照。

エラーの有無を確認

dataを使う前にerrorがあるか確認するのがベストです。これにより事前にエラーを処理でき、dataundefinedチェックが不要になります。

ts
const { data, error } = await actions.example();if (error) {  // エラー処理  return;}// dataを利用

エラーチェックを行わずにdataへ直接アクセスする

プロトタイピング中、またはエラーを自動的に捕捉してくれるライブラリを使用している場合など、エラー処理を省きたい場合は、アクション呼び出しに.orThrow()プロパティを付与します。errorを返す代わりに例外をスローし、アクションのdataを直接返します。

以下の例ではlikePost()アクションを呼び出し、ハンドラーから返された更新後のいいね数をnumber型として受け取ります。

ts
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });//    ^ 型: number

アクション内でバックエンドエラーを処理する

データベースエントリが存在しない場合の"not found"や、ユーザーがログインしていない場合の"unauthorized"など、アクションのhandler()からエラーをスローするには、ActionErrorを使用します。undefinedを返す方法と比べて、次の2点がメリットです。

  • 404 - Not found401 - Unauthorizedのようにステータスコードを設定できます。これにより開発環境・本番環境の両方でリクエストごとのステータスを確認しやすくなります。

  • アプリケーション側では、すべてのエラーがアクション結果のerrorオブジェクトにまとめられるため、undefinedチェックが不要になり、原因に応じたフィードバックをユーザーに表示できます。

ActionErrorを作成する

エラーをスローするには、astro:actionsモジュールからActionErrorクラスをインポートします。人が読めるcode(例:"NOT_FOUND""BAD_REQUEST")と、任意で詳細を示すmessageを渡します。

次の例では、認証用Cookie「user-session」が存在しない場合に、likePostアクションからUNAUTHORIZEDエラーをスローしています。

ts
import { defineAction, ActionError } from "astro:actions";import { z } from "astro:schema";export const server = {  likePost: defineAction({    input: z.object({ postId: z.string() }),    handler: async (input, ctx) => {      if (!ctx.cookies.has('user-session')) {        throw new ActionError({          code: "UNAUTHORIZED",          message: "User must be logged in.",        });      }      // ここで投稿に「いいね」を付与    },  }),};

ActionErrorを処理する

アプリケーションからアクションを呼び出し、errorプロパティの有無をチェックします。このプロパティはActionError型で、codemessageを含みます。

次の例ではLikeButton.tsxコンポーネントがクリック時にlikePost()を呼び出し、認証エラーならログインリンクを表示します。

tsx
import { actions } from 'astro:actions';import { useState } from 'preact/hooks';export function LikeButton({ postId }: { postId: string }) {  const [showLogin, setShowLogin] = useState(false);  return (    <>      {        showLogin && <a href="/signin">Log in to like a post.</a>      }      <button onClick={async () => {        const { data, error } = await actions.likePost({ postId });        if (error?.code === 'UNAUTHORIZED') setShowLogin(true);        // 予期しないエラーは早期リターン        else if (error) return;        // いいね数を更新      }}>        Like      </button>    </>  )}

クライアントリダイレクトを処理する

クライアント側からアクションを呼び出す場合、react-routerのようなライブラリと連携するか、Astroのnavigate()関数を使用して、アクション成功時に新しいページへリダイレクトできます。

以下の例では、logoutアクションが成功した後、ホームページへ遷移します。

tsx
import { actions } from 'astro:actions';import { navigate } from 'astro:transitions/client';export function LogoutButton() {  return (    <button onClick={async () => {      const { error } = await actions.logout();      if (!error) navigate('/');    }}>      Logout    </button>  );}

アクションでフォームデータを受け取る

アクションはデフォルトでJSONデータを受け取ります。HTMLフォームのデータを扱いたい場合は、defineAction()accept: 'form'を指定します。

ts
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  comment: defineAction({    accept: 'form',    input: z.object(/* ... */),    handler: async (input) => { /* ... */ },  })}

フォームデータのバリデーション

アクションは送信されたフォームデータを、各入力のname属性をキーとするオブジェクトへ変換します。たとえば<input name="search">を含むフォームは{ search: 'user input' }のように解析されます。このオブジェクトはアクションのinputスキーマで検証されます。

ハンドラーで生のFormDataオブジェクトを扱いたい場合は、アクション定義からinputプロパティを省略してください。

以下は、メールアドレス入力と「利用規約に同意」チェックボックスを検証するニュースレター登録フォームの例です。

  1. 各入力に一意のname属性を持つHTMLフォームコンポーネントを作成します。

    astro
    <form>  <label for="email">E-mail</label>  <input id="email" required type="email" name="email" />  <label>    <input required type="checkbox" name="terms">    I agree to the terms of service  </label>  <button>Sign up</button></form>
  2. 送信されたフォームを処理するnewsletterアクションを定義します。emailフィールドはz.string().email()で、termsチェックボックスはz.boolean()で検証します。

    ts
    import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  newsletter: defineAction({    accept: 'form',    input: z.object({      email: z.string().email(),      terms: z.boolean(),    }),    handler: async ({ email, terms }) => { /* ... */ },  })}

    inputで利用できるすべてのフォームバリデータはinput APIリファレンスを参照してください。

  3. フォームに<script>を追加し、ユーザー入力を送信します。この例ではフォームの既定の送信動作を上書きしてactions.newsletter()を呼び出し、成功時に/confirmationへリダイレクトします。

    astro
    <form>  <label for="email">E-mail</label>  <input id="email" required type="email" name="email" />  <label>    <input required type="checkbox" name="terms">    I agree to the terms of service  </label>  <button>Sign up</button></form><script>  import { actions } from 'astro:actions';  import { navigate } from 'astro:transitions/client';  const form = document.querySelector('form');  form?.addEventListener('submit', async (event) => {    event.preventDefault();    const formData = new FormData(form);    const { error } = await actions.newsletter(formData);    if (!error) navigate('/confirmation');  })</script>

    フォームデータを送信する別の方法は「HTMLフォームのactionからアクションを呼び出す」を参照してください。

フォーム入力エラーを表示する

requiredtype="email"patternなどのネイティブHTMLフォームバリデーション属性で送信前に検証できます。バックエンドでより複雑なinput検証を行う場合は、isInputError()ユーティリティを使用します。

入力エラーを取得するには、isInputError()でエラー原因が入力不正か確認します。入力エラーはfieldsオブジェクトに検証に失敗した各入力名のメッセージを持ちます。これらのメッセージを使ってユーザーに修正を促せます。

次の例ではisInputError()でエラーを確認し、メールフィールドにエラーがあるかをチェックしてメッセージを生成しています。DOM操作や任意のUIフレームワークでユーザーに表示してください。

js
import { actions, isInputError } from 'astro:actions';const form = document.querySelector('form');const formData = new FormData(form);const { error } = await actions.newsletter(formData);if (isInputError(error)) {  // 入力エラーを処理  if (error.fields.email) {    const message = error.fields.email.join(', ');  }}

HTMLフォームのactionからアクションを呼び出す

:::note フォームのactionでアクションを呼び出すページはオンデマンドレンダリングが必要です。このAPIを使用する前に、そのページで事前レンダリングを無効化してください。 :::

任意の<form>要素に標準属性を追加するだけで、ゼロJS(JavaScript不要)のフォーム送信を実現できます。クライアント側JavaScriptが読み込まれなかった場合のフォールバックとして、あるいはフォーム処理を完全にサーバーに任せたい場合に便利です。

サーバーでAstro.getActionResult()を呼び出すと、フォーム送信の結果(dataまたはerror)を取得できます。これを使ってリダイレクトやエラーハンドリング、UI更新などを動的に行えます。

HTMLフォームからアクションを呼び出すには、<form>method="POST"を追加し、action属性にアクションを設定します。たとえばaction={actions.logout}とすると、サーバー側で自動処理されるクエリ文字列がaction属性へ設定されます。

次のAstroコンポーネントは、ボタンをクリックするとlogoutアクションを呼び出し、現在のページを再読み込みします。

astro
---import { actions } from 'astro:actions';---<form method="POST" action={actions.logout}>  <button>Log out</button></form>

アクション成功時にリダイレクトする

アクション成功後に新しいルートへリダイレクトしたい場合は、サーバー側でアクション結果を利用します。よくある例として、商品レコードを作成した後に/products/[id]へリダイレクトするパターンがあります。

たとえば、生成された商品IDを返すcreateProductアクションがあるとします。

ts
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {  createProduct: defineAction({    accept: 'form',    input: z.object({ /* ... */ }),    handler: async (input) => {      const product = await persistToDatabase(input);      return { id: product.id };    },  })}

AstroコンポーネントでAstro.getActionResult()を呼び出してアクション結果を取得します。アクションが呼び出されていない場合はundefined、呼び出されていればdataまたはerrorを含むオブジェクトが返ります。

dataプロパティを使ってURLを作成し、Astro.redirect()でリダイレクトします。

astro
---import { actions } from 'astro:actions';const result = Astro.getActionResult(actions.createProduct);if (result && !result.error) {  return Astro.redirect(`/products/${result.data.id}`);}---<form method="POST" action={actions.createProduct}>  <!--...--></form>

フォームactionのエラーを処理する

フォームを含むAstroコンポーネント内でAstro.getActionResult()を呼び出すと、カスタムエラーハンドリング用にdataerrorへアクセスできます。

次の例では、newsletterアクションが失敗したときに一般的なエラーメッセージを表示します。

astro
---import { actions } from 'astro:actions';const result = Astro.getActionResult(actions.newsletter);---{result?.error && (  <p class="error">Unable to sign up. Please try again later.</p>)}<form method="POST" action={actions.newsletter}>  <label>    E-mail    <input required type="email" name="email" />  </label>  <button>Sign up</button></form>

より細かい制御を行う場合は、 isInputError()ユーティリティを使って入力不正が原因かどうかを判定できます。

次の例は、無効なメールアドレスが送信されたときにemail入力欄の下へエラーバナーを表示します。

astro
---import { actions, isInputError } from 'astro:actions';const result = Astro.getActionResult(actions.newsletter);const inputErrors = isInputError(result?.error) ? result.error.fields : {};---<form method="POST" action={actions.newsletter}>  <label>    E-mail    <input required type="email" name="email" aria-describedby="error" />  </label>  {inputErrors.email && <p id="error">{inputErrors.email.join(',')}</p>}  <button>Sign up</button></form>

エラー時に入力値を保持する

フォーム送信時、入力値はクリアされます。値を保持したい場合は、ページでビュートランジションを有効化し、各入力にtransition:persistディレクティブを追加します。

astro
<input transition:persist required type="email" name="email" />

フォームアクション結果でUIを更新する

アクションの戻り値を用いて成功時に通知を表示するには、アクションをAstro.getActionResult()へ渡し、返されたdataで必要なUIをレンダリングします。

次の例では、addToCartアクションが返すproductNameを使い、成功メッセージを表示しています。

astro
---import { actions } from 'astro:actions';const result = Astro.getActionResult(actions.addToCart);---{result && !result.error && (  <p class="success">Added {result.data.productName} to cart</p>)}<!--...-->

上級編: セッションにアクション結果を保持する

アクション結果はPOST送信として表示されるため、ユーザーがページを閉じて再訪問すると結果はundefinedに戻ります。また、ページをリロードしようとすると「フォームの再送信を確認しますか?」ダイアログが表示されます。

この挙動をカスタマイズするには、ミドルウェアを追加してアクション結果を手動で処理します。Cookieやセッションストレージを用いて結果を保持する方法を選択できます。

まずミドルウェアファイルを作成し、astro:actionsgetActionContext()ユーティリティをインポートします。この関数はアクションハンドラーや呼び出し元(HTMLフォームかどうか)などの情報を含むactionオブジェクトを返します。また、setActionResult()serializeActionResult()を返し、Astro.getActionResult()で参照する値をプログラム的に設定できます。

ts
import { defineMiddleware } from 'astro:middleware';import { getActionContext } from 'astro:actions';export const onRequest = defineMiddleware(async (context, next) => {  const { action, setActionResult, serializeActionResult } = getActionContext(context);  if (action?.calledFrom === 'form') {    const result = await action.handler();    // ... アクション結果を処理    setActionResult(action.name, serializeActionResult(result));  }  return next();});

HTMLフォーム結果を保持する一般的な手法はPOST / Redirect / GETパターンです。これによりリロード時の再送信ダイアログが消え、アクション結果をセッション全体で維持できます。

以下の例では、Netlifyサーバーアダプターを使用し、セッションストレージにPOST / Redirect / GETを適用しています。アクション結果をNetlify Blobへ保存し、リダイレクト後にセッションIDで取得しています。

ts
import { defineMiddleware } from 'astro:middleware';import { getActionContext } from 'astro:actions';import { randomUUID } from "node:crypto";import { getStore } from "@netlify/blobs";export const onRequest = defineMiddleware(async (context, next) => {  // プリレンダーページのリクエストはスキップ  if (context.isPrerendered) return next();    const { action, setActionResult, serializeActionResult } =    getActionContext(context);  // Netlify Blobでアクション結果を保持するストアを作成  const actionStore = getStore("action-session");    // Cookie経由で結果が渡された場合、Astro.getActionResult()で参照できるよう設定  const sessionId = context.cookies.get("action-session-id")?.value;  const session = sessionId    ? await actionStore.get(sessionId, { type: "json" })    : undefined;    if (session) {    setActionResult(session.actionName, session.actionResult);    // 必要なら描画後にセッションを削除    await actionStore.delete(sessionId);    context.cookies.delete("action-session-id");    return next();  }    // HTMLフォームのactionから呼び出された場合  if (action?.calledFrom === "form") {    const actionResult = await action.handler();      // アクション結果をセッションストレージへ保持    const newSessionId = randomUUID();    await actionStore.setJSON(newSessionId, {      actionName: action.name,      actionResult: serializeActionResult(actionResult),    });      // リダイレクト後に取得できるようCookieへセッションIDを設定    context.cookies.set("action-session-id", newSessionId);      // エラー時は前のページへリダイレクト    if (actionResult.error) {      const referer = context.request.headers.get("Referer");      if (!referer) {        throw new Error(          "Internal: Referer unexpectedly missing from Action POST request.",        );      }      return context.redirect(referer);    }    // 成功時は現在のパスへリダイレクト    return context.redirect(context.originPathname);  }    return next();});

アクション利用時のセキュリティ

アクションはアクション名に基づくパブリックエンドポイントとして公開されます。たとえばblog.like()アクションは/_actions/blog.likeからアクセス可能です。これはユニットテストや本番エラーのデバッグに便利ですが、APIエンドポイントやオンデマンドレンダリングページと同じ認可チェックを必ず実装する必要があります。

アクションハンドラー内でユーザーを認可する

アクションリクエストを認可するには、アクションのhandler()に認証チェックを追加します。セッション管理やユーザー情報の取得には認証ライブラリの利用を検討してください。

アクションではミドルウェアから渡された値にアクセスできるAPIContext全体が利用できます。ユーザーが認可されていない場合は、UNAUTHORIZEDコードでActionErrorをスローします。

ts
import { defineAction, ActionError } from 'astro:actions';export const server = {  getUserSettings: defineAction({    handler: async (_input, context) => {      if (!context.locals.user) {        throw new ActionError({ code: 'UNAUTHORIZED' });      }      return { /* 成功時のデータ */ };    }  })}

ミドルウェアでアクションを制限する

Astroでは、各アクションのhandler()内でユーザーセッションを認可し、パーミッションやレート制限をアクションごとに設定することを推奨しています。しかし、ミドルウェアから一括で(または一部の)アクションリクエストを制御することも可能です。

ミドルウェアでgetActionContext()を使用して、受信したアクションリクエストの情報(アクション名や呼び出し元がRPCかHTMLフォームかなど)を取得します。

次の例では、有効なセッショントークンがないすべてのアクションリクエストを拒否します。チェックに失敗すると403 Forbiddenを返します。この方法はセッションが存在する場合のみアクセスを許可しますが、安全な認可の代替にはなりません。

ts
import { defineMiddleware } from 'astro:middleware';import { getActionContext } from 'astro:actions';export const onRequest = defineMiddleware(async (context, next) => {  const { action } = getActionContext(context);  // クライアントサイドRPCから呼び出されたアクションか確認  if (action?.calledFrom === 'rpc') {    // セッショントークンをチェック    if (!context.cookies.has('user-session')) {      return new Response('Forbidden', { status: 403 });    }  }    context.cookies.set('user-session', /* セッショントークン */);  return next();});

Astroコンポーネントやサーバーエンドポイントからアクションを呼び出す

Astro.callAction()(サーバーエンドポイントではcontext.callAction())ラッパーを使用して、Astroコンポーネント内のスクリプトや他のサーバーコードからアクションを直接呼び出せます。アクションのロジックを再利用する際によく使われます。

第1引数にアクション、第2引数に入力パラメータを渡します。返り値はクライアント側と同様にdataerrorを含むオブジェクトです。

astro
---import { actions } from 'astro:actions';const searchQuery = Astro.url.searchParams.get('search');if (searchQuery) {  const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery });  // 結果を処理}---