Actions
Added in:
astro@4.15
Astroアクションを使うと、型安全なバックエンド関数を定義し、どこからでも呼び出せます。データ取得やJSON解析、入力バリデーションを自動で行うため、APIエンドポイントを使う場合に比べてボイラープレートを大幅に削減できます。
APIエンドポイントの代わりにアクションを使うと、クライアントとサーバーコード間の通信がシームレスになり、次のメリットがあります。
- ZodバリデーションでJSONやフォームデータを自動検証。
- バックエンドを呼び出す型安全な関数を自動生成。さらにHTMLフォームの
action
属性からも呼び出せます。手動でfetch()
を書く必要はありません。 ActionError
(EN)でバックエンドエラーを標準化。
基本的な使い方
Section titled 基本的な使い方アクションはsrc/actions/index.ts
でserver
オブジェクトとしてエクスポートします。
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
export const server = { myAction: defineAction({ /* ... */ })}
定義したアクションはastro:actions
モジュールから関数として利用できます。UIフレームワークコンポーネント内、フォームのPOSTリクエスト、またはAstroコンポーネント内の<script>
タグで呼び出してください。
アクションを呼び出すと、data
(JSON直列化された結果)またはerror
(スローされたエラー)が入ったオブジェクトが返ります。
------
<script>import { actions } from 'astro:actions';
async () => { const { data, error } = await actions.myAction({ /* ... */ });}</script>
最初のアクションを書いてみる
Section titled 最初のアクションを書いてみる以下の手順でアクションを定義し、Astroページのscript
タグから呼び出します。
-
src/actions/index.ts
を作成し、server
オブジェクトをエクスポートします。src/actions/index.ts export const server = {// アクションをここに宣言} -
defineAction()
ユーティリティをastro:actions
から、z
オブジェクトをastro:schema
からインポートします。src/actions/index.ts import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {// アクションをここに宣言} -
defineAction()
でgetGreeting
アクションを定義します。input
プロパティはZodスキーマで入力を検証し、handler()
がサーバーで実行されるバックエンドロジックです。src/actions/index.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}!`}})} -
ボタンをクリックすると
getGreeting
アクションで挨拶を取得するAstroコンポーネントを作成します。src/pages/index.astro ------<button>Get greeting</button><script>const button = document.querySelector('button');button?.addEventListener('click', async () => {// アクションから取得した挨拶を表示});</script> -
actions
をastro:actions
からインポートし、クリックハンドラ内でactions.getGreeting()
を呼び出します。name
オプションがサーバー側のhandler()
に渡され、エラーがなければ結果がdata
として返ります。src/pages/index.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()
(EN)の全プロパティはAPIリファレンスを参照してください。
アクションの整理方法
Section titled アクションの整理方法すべてのアクションはsrc/actions/index.ts
のserver
オブジェクトからエクスポートする必要があります。定義をそのまま書いても、別ファイルへ切り出してインポートしても構いません。関連する関数をネストしてまとめることもできます。
たとえば、ユーザー関連のアクションをまとめる場合、src/actions/user.ts
にgetUser
とcreateUser
をまとめたuser
オブジェクトを作成します。
import { defineAction } from 'astro:actions';
export const user = { getUser: defineAction(/* ... */), createUser: defineAction(/* ... */),}
その後、src/actions/index.ts
でインポートし、他のアクションと並べてトップレベルに追加します。
import { user } from './user';
export const server = { myAction: defineAction({ /* ... */ }), user,}
これでユーザー関連アクションはactions.user
から呼び出せます。
actions.user.getUser()
actions.user.createUser()
返り値の扱い
Section titled 返り値の扱いアクションはhandler()
の型安全な戻り値を持つdata
、またはバックエンドエラーを持つerror
を返します。error
はinput
のバリデーションエラーやhandler()
内でスローされたエラーです。
アクションはDates、Maps、Sets、URLsを扱える独自フォーマットで返します(Devalueライブラリ使用)。そのため通常のJSONのようにネットワークレスポンスを簡単に検査できません。デバッグ時はアクションが返すdata
オブジェクトを確認してください。
エラーの有無を確認
Section titled エラーの有無を確認data
を使う前にerror
があるか確認するのがベストです。これにより事前にエラーを処理でき、data
のundefined
チェックが不要になります。
const { data, error } = await actions.example();
if (error) { // エラー処理 return;}// dataを利用
エラーチェックを行わずにdata
へ直接アクセスする
Section titled エラーチェックを行わずにdataへ直接アクセスするプロトタイピング中、またはエラーを自動的に捕捉してくれるライブラリを使用している場合など、エラー処理を省きたい場合は、アクション呼び出しに.orThrow()
プロパティを付与します。error
を返す代わりに例外をスローし、アクションのdata
を直接返します。
以下の例ではlikePost()
アクションを呼び出し、ハンドラーから返された更新後のいいね数をnumber
型として受け取ります。
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });// ^ 型: number
アクション内でバックエンドエラーを処理する
Section titled アクション内でバックエンドエラーを処理するデータベースエントリが存在しない場合の”not found”や、ユーザーがログインしていない場合の”unauthorized”など、アクションのhandler()
からエラーをスローするには、ActionError
を使用します。undefined
を返す方法と比べて、次の2点がメリットです。
-
404 - Not found
や401 - Unauthorized
のようにステータスコードを設定できます。これにより開発環境・本番環境の両方でリクエストごとのステータスを確認しやすくなります。 -
アプリケーション側では、すべてのエラーがアクション結果の
error
オブジェクトにまとめられるため、undefined
チェックが不要になり、原因に応じたフィードバックをユーザーに表示できます。
ActionError
を作成する
Section titled ActionErrorを作成するエラーをスローするには、astro:actions
モジュールからActionError
クラスをインポートします。人が読めるcode
(例:"NOT_FOUND"
や"BAD_REQUEST"
)と、任意で詳細を示すmessage
を渡します。
次の例では、認証用Cookie「user-session」が存在しない場合に、likePost
アクションからUNAUTHORIZED
エラーをスローしています。
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
を処理する
Section titled ActionErrorを処理するアプリケーションからアクションを呼び出し、error
プロパティの有無をチェックします。このプロパティはActionError
型で、code
とmessage
を含みます。
次の例ではLikeButton.tsx
コンポーネントがクリック時にlikePost()
を呼び出し、認証エラーならログインリンクを表示します。
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> </> )}
クライアントリダイレクトを処理する
Section titled クライアントリダイレクトを処理するクライアント側からアクションを呼び出す場合、react-router
のようなライブラリと連携するか、Astroのnavigate()
関数を使用して、アクション成功時に新しいページへリダイレクトできます。
以下の例では、logout
アクションが成功した後、ホームページへ遷移します。
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> );}
アクションでフォームデータを受け取る
Section titled アクションでフォームデータを受け取るアクションはデフォルトでJSONデータを受け取ります。HTMLフォームのデータを扱いたい場合は、defineAction()
でaccept: 'form'
を指定します。
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
export const server = { comment: defineAction({ accept: 'form', input: z.object(/* ... */), handler: async (input) => { /* ... */ }, })}
フォームデータのバリデーション
Section titled フォームデータのバリデーションアクションは送信されたフォームデータを、各入力のname
属性をキーとするオブジェクトへ変換します。たとえば<input name="search">
を含むフォームは{ search: 'user input' }
のように解析されます。このオブジェクトはアクションのinput
スキーマで検証されます。
ハンドラーで生のFormData
オブジェクトを扱いたい場合は、アクション定義からinput
プロパティを省略してください。
以下は、メールアドレス入力と「利用規約に同意」チェックボックスを検証するニュースレター登録フォームの例です。
-
各入力に一意の
name
属性を持つHTMLフォームコンポーネントを作成します。src/components/Newsletter.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> -
送信されたフォームを処理する
newsletter
アクションを定義します。email
フィールドはz.string().email()
で、terms
チェックボックスはz.boolean()
で検証します。src/actions/index.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リファレンス (EN)を参照してください。 -
フォームに
<script>
を追加し、ユーザー入力を送信します。この例ではフォームの既定の送信動作を上書きしてactions.newsletter()
を呼び出し、成功時に/confirmation
へリダイレクトします。src/components/Newsletter.astro <form>7 collapsed lines<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
からアクションを呼び出す」を参照してください。
フォーム入力エラーを表示する
Section titled フォーム入力エラーを表示するrequired
、type="email"
、pattern
などのネイティブHTMLフォームバリデーション属性で送信前に検証できます。バックエンドでより複雑なinput
検証を行う場合は、isInputError()
(EN)ユーティリティを使用します。
入力エラーを取得するには、isInputError()
でエラー原因が入力不正か確認します。入力エラーはfields
オブジェクトに検証に失敗した各入力名のメッセージを持ちます。これらのメッセージを使ってユーザーに修正を促せます。
次の例ではisInputError()
でエラーを確認し、メールフィールドにエラーがあるかをチェックしてメッセージを生成しています。DOM操作や任意のUIフレームワークでユーザーに表示してください。
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
からアクションを呼び出す
Section titled HTMLフォームのactionからアクションを呼び出すフォームのaction
でアクションを呼び出すページはオンデマンドレンダリングが必要です。このAPIを使用する前に、そのページで事前レンダリングを無効化してください。
任意の<form>
要素に標準属性を追加するだけで、ゼロJS(JavaScript不要)のフォーム送信を実現できます。クライアント側JavaScriptが読み込まれなかった場合のフォールバックとして、あるいはフォーム処理を完全にサーバーに任せたい場合に便利です。
サーバーでAstro.getActionResult() (EN)を呼び出すと、フォーム送信の結果(data
またはerror
)を取得できます。これを使ってリダイレクトやエラーハンドリング、UI更新などを動的に行えます。
HTMLフォームからアクションを呼び出すには、<form>
にmethod="POST"
を追加し、action
属性にアクションを設定します。たとえばaction={actions.logout}
とすると、サーバー側で自動処理されるクエリ文字列がaction
属性へ設定されます。
次のAstroコンポーネントは、ボタンをクリックするとlogout
アクションを呼び出し、現在のページを再読み込みします。
---import { actions } from 'astro:actions';---
<form method="POST" action={actions.logout}> <button>Log out</button></form>
アクション成功時にリダイレクトする
Section titled アクション成功時にリダイレクトするアクション成功後に新しいルートへリダイレクトしたい場合は、サーバー側でアクション結果を利用します。よくある例として、商品レコードを作成した後に/products/[id]
へリダイレクトするパターンがあります。
たとえば、生成された商品IDを返すcreateProduct
アクションがあるとします。
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()
でリダイレクトします。
---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
のエラーを処理する
Section titled フォームactionのエラーを処理するフォームを含むAstroコンポーネント内でAstro.getActionResult()
を呼び出すと、カスタムエラーハンドリング用にdata
とerror
へアクセスできます。
次の例では、newsletter
アクションが失敗したときに一般的なエラーメッセージを表示します。
---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
入力欄の下へエラーバナーを表示します。
---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>
エラー時に入力値を保持する
Section titled エラー時に入力値を保持するフォーム送信時、入力値はクリアされます。値を保持したい場合は、ページでビュートランジションを有効化し、各入力にtransition:persist
ディレクティブを追加します。
<input transition:persist required type="email" name="email" />
フォームアクション結果でUIを更新する
Section titled フォームアクション結果でUIを更新するアクションの戻り値を用いて成功時に通知を表示するには、アクションをAstro.getActionResult()
へ渡し、返されたdata
で必要なUIをレンダリングします。
次の例では、addToCart
アクションが返すproductName
を使い、成功メッセージを表示しています。
---import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.addToCart);---
{result && !result.error && ( <p class="success">Added {result.data.productName} to cart</p>)}
<!--...-->
上級編: セッションにアクション結果を保持する
Section titled 上級編: セッションにアクション結果を保持する
Added in:
astro@5.0.0
アクション結果はPOST送信として表示されるため、ユーザーがページを閉じて再訪問すると結果はundefined
に戻ります。また、ページをリロードしようとすると「フォームの再送信を確認しますか?」ダイアログが表示されます。
この挙動をカスタマイズするには、ミドルウェアを追加してアクション結果を手動で処理します。Cookieやセッションストレージを用いて結果を保持する方法を選択できます。
まずミドルウェアファイルを作成し、astro:actions
のgetActionContext() (EN)ユーティリティをインポートします。この関数はアクションハンドラーや呼び出し元(HTMLフォームかどうか)などの情報を含むaction
オブジェクトを返します。また、setActionResult()
とserializeActionResult()
を返し、Astro.getActionResult()
で参照する値をプログラム的に設定できます。
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サーバーアダプター (EN)を使用し、セッションストレージにPOST / Redirect / GETを適用しています。アクション結果をNetlify Blobへ保存し、リダイレクト後にセッションIDで取得しています。
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();});
アクション利用時のセキュリティ
Section titled アクション利用時のセキュリティアクションはアクション名に基づくパブリックエンドポイントとして公開されます。たとえばblog.like()
アクションは/_actions/blog.like
からアクセス可能です。これはユニットテストや本番エラーのデバッグに便利ですが、APIエンドポイントやオンデマンドレンダリングページと同じ認可チェックを必ず実装する必要があります。
アクションハンドラー内でユーザーを認可する
Section titled アクションハンドラー内でユーザーを認可するアクションリクエストを認可するには、アクションのhandler()
に認証チェックを追加します。セッション管理やユーザー情報の取得には認証ライブラリの利用を検討してください。
アクションではミドルウェアから渡された値にアクセスできるAPIContext
全体が利用できます。ユーザーが認可されていない場合は、UNAUTHORIZED
コードでActionError
をスローします。
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 { /* 成功時のデータ */ }; } })}
ミドルウェアでアクションを制限する
Section titled ミドルウェアでアクションを制限する
Added in:
astro@5.0.0
Astroでは、各アクションのhandler()
内でユーザーセッションを認可し、パーミッションやレート制限をアクションごとに設定することを推奨しています。しかし、ミドルウェアから一括で(または一部の)アクションリクエストを制御することも可能です。
ミドルウェアでgetActionContext()
を使用して、受信したアクションリクエストの情報(アクション名や呼び出し元がRPCかHTMLフォームかなど)を取得します。
次の例では、有効なセッショントークンがないすべてのアクションリクエストを拒否します。チェックに失敗すると403 Forbidden
を返します。この方法はセッションが存在する場合のみアクセスを許可しますが、安全な認可の代替にはなりません。
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コンポーネントやサーバーエンドポイントからアクションを呼び出す
Section titled Astroコンポーネントやサーバーエンドポイントからアクションを呼び出すAstro.callAction()
(サーバーエンドポイントではcontext.callAction()
)ラッパーを使用して、Astroコンポーネント内のスクリプトや他のサーバーコードからアクションを直接呼び出せます。アクションのロジックを再利用する際によく使われます。
第1引数にアクション、第2引数に入力パラメータを渡します。返り値はクライアント側と同様にdata
とerror
を含むオブジェクトです。
---import { actions } from 'astro:actions';
const searchQuery = Astro.url.searchParams.get('search');if (searchQuery) { const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery }); // 結果を処理}---