From 4ac76b8f8a127479fd6daedbd4117861f1a58ae5 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 1 Aug 2024 14:23:35 -0400 Subject: [PATCH 01/66] feat: add actions to nav --- src/i18n/en/nav.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/en/nav.ts b/src/i18n/en/nav.ts index 0bab2901f520f..79ac1e7e3968f 100644 --- a/src/i18n/en/nav.ts +++ b/src/i18n/en/nav.ts @@ -53,6 +53,7 @@ export default [ { text: 'Routes and Navigation', header: true, type: 'learn', key: 'routes' }, { text: 'Routing', slug: 'guides/routing', key: 'guides/routing' }, { text: 'Endpoints', slug: 'guides/endpoints', key: 'guides/endpoints' }, + { text: 'Actions', slug: 'guides/actions', key: 'guides/actions' }, { text: 'Prefetch', slug: 'guides/prefetch', key: 'guides/prefetch' }, { text: 'Middleware', slug: 'guides/middleware', key: 'guides/middleware' }, { From ed2b43f6aa3f48a21bebd5b2fdc90df2bb518fd7 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 1 Aug 2024 14:23:39 -0400 Subject: [PATCH 02/66] feat: basic usage guide --- src/content/docs/en/guides/actions.mdx | 63 ++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/content/docs/en/guides/actions.mdx diff --git a/src/content/docs/en/guides/actions.mdx b/src/content/docs/en/guides/actions.mdx new file mode 100644 index 0000000000000..c967afa298cfc --- /dev/null +++ b/src/content/docs/en/guides/actions.mdx @@ -0,0 +1,63 @@ +--- +title: Actions +description: Learn how to create type-safe server functions you can call from anywhere. +i18nReady: true +--- + +import { Steps } from '@astrojs/starlight/components'; + + +Astro actions make it easy to define and call backend functions with type-safety. + +# Basic usage + + + +1. Create an `src/actions/index.ts|js` file. + +2. Inside this file, export a `server` object. Each key in this object corresponds to an action name: + + ```ts title="src/actions/index.ts" + export const server = { + // action declarations + } + ``` + +3. Import the `defineAction()` utility from `astro:actions`, and the `z` object from `astro:schema`. `defineAction()` accepts a `handler` function containing any backend logic you want to run, and an optional `input` object to validate input parameters with Zod. Anything returned from the `handler` function will be serialized as a response. + + ```ts ins={1-2, 5-12} title="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}!` + } + }) + } + ``` + +4. Call your action from client code by importing `actions` from `astro:actions`. This object contains all functions exported by the `server` object. For example, call `actions.getGreeting()` on a button press using a ` + ``` + + From 78110ad4eb32cfbcbfee77459ca7be2d18809399 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 1 Aug 2024 17:04:15 -0400 Subject: [PATCH 03/66] docs: describe how errors are handled --- src/content/docs/en/guides/actions.mdx | 145 ++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/src/content/docs/en/guides/actions.mdx b/src/content/docs/en/guides/actions.mdx index c967afa298cfc..56a6f276bd86c 100644 --- a/src/content/docs/en/guides/actions.mdx +++ b/src/content/docs/en/guides/actions.mdx @@ -54,10 +54,153 @@ Astro actions make it easy to define and call backend functions with type-safety document.querySelector('button')?.addEventListener('click', async () => { const { data, error } = await actions.getGreeting({ name: "Houston" }); - alert(greeting); + alert(data); // Show alert pop-up with "Hello, Houston!" }) ``` + +## Organizing actions + +All actions in your project are exported from the `server` object. You may keep all action definitions as top-level keys inside your `src/actions/index.ts` file, or you may group related functions in nested objects. + +A common pattern is to organize actions based the data that action works with. For example, you may have related `getUser()` and `createUser()` actions that operate on a user. You can move these to a separate file in your source directory (like `src/actions/user.ts`), and nest actions in a related object like `user`: + +```ts +// src/actions/user.ts +import { defineAction } from 'astro:actions'; + +export const user = { + getUser: defineAction(/* ... */), + createUser: defineAction(/* ... */), +} +``` + +Then, you can import this `user` object into your `actions/index` file and apply to the `server` object: + +```ts +import { user } from './user'; + +export const server = { + user, +} +``` + +Now, all user actions are callable from the `actions.user` object: + +- `actions.user.getUser()` +- `actions.user.createUser()` + +## Defining actions + +Actions are defined using the `defineAction()` function from the `astro:actions` module. This function accepts a `handler` function for backend logic you want to run, and an optional `input` object to validate inputs at runtime with Zod. + +## Handling action return values + +Actions return an object containing either `data` with the type-safe return value of your `handler()`, or an `error` with any backend errors. Errors may come from validation errors on the `input` property or thrown errors within the `handler()`. + +It's best to check if an `error` is present before reading the `data` property. This allows you to handle errors up-front, and ensure `data` is defined without an `undefined` check. + +```ts +const { data, error } = await actions.example(); + +if (error) { + // handle error cases + return; +} +// use `data` +``` + +### Accessing `data` directly without an error check + +You may have simpler actions that do you not need to throw errors. In these cases, you may prefer to get data directly and allow unexpected errors to throw. + +Use the `.orThrow()` extension function on your action call to access `data` directly. This example calls a `likePost` action that returns the updated number of likes as a `number` from the action `handler`: + +```ts ins="orThrow" +const updatedLikes = await actions.likePost.orThrow({ postId: 'example' }); +// type: number +``` + +### Raising and handling errors + +You may need to throw an error from your action `handler()`. This includes "not found" errors when a database entry is missing, "unauthorized" errors when a user is not logged in, etc. + +It may be tempting to return `undefined` in these cases. However, Astro recommends using the `AstroError` object to throw an error instead. This offers a few benefits: + +- You can set a status code like `404 - Not found` and `401 - Unauthorized`. This improves debugging errors in development and in production by letting you see the status code of each request. + +- In your application code, all errors are passed to the `error` object on an action result. This avoids the need for `undefined` checks on data, and allows you to display targeted feedback to the user depending on what went wrong. + +To throw an error, import the `ActionError()` class from the `astro:actions` module. `ActionError()` accepts as `code` containing a human-readable status code (like `"NOT_FOUND"` or `"BAD_REQUEST"`), and an optional `message` field to provide further information about the error. + +This example throws an error from a `likePost()` action when a user is not logged in, checking a hypothetical "user-session" cookie from an authentication service: + +```ts title="src/actions/index.ts" ins="ActionError" +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.", + }); + } + // Otherwise, like the post + }, + }), +}; +``` + +To handle this error, you can call the action from your application and check whether an `error` is present. This property will be of type `ActionError`, and you can use the `error.code` attribute to display different UI depending on the error that is thrown. + +This example [uses a Preact component](/en/guides/integrations-guide/preact/) to implement a like button. If an authentication error occurs, it will display a log in link to the user: + +```tsx title=src/components/LikeButton.tsx ins="if (error.code === 'UNAUTHORIZED') setShowLogin(true);" +import { actions } from 'astro:actions'; +import { useState } from 'preact/hooks'; + +export function LikeButton({ postId }: { postId: string }) { + const [showLogin, setShowLogin] = useState(false); + return ( + <> + { + showLogin && Log in to like a post. + } + + + ) +} +``` + + +## Accepting form data from an action + +Actions accept JSON data by default. You can switch an action to accept `FormData` by adding the `accept: 'form'` paremeter to your `defineAction()` call: + +```ts ins="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) => { /* ... */ }, + }) +} +``` From 2b2b9ab2a4aaa6ca8d425600bf6900935a975d8b Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 1 Aug 2024 18:13:51 -0400 Subject: [PATCH 04/66] docs: input validator API reference --- src/content/docs/en/reference/api-reference.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/content/docs/en/reference/api-reference.mdx b/src/content/docs/en/reference/api-reference.mdx index be30938da1f20..3cb146b82d7af 100644 --- a/src/content/docs/en/reference/api-reference.mdx +++ b/src/content/docs/en/reference/api-reference.mdx @@ -2054,3 +2054,19 @@ This component provides a way to inspect values on the client-side, without any [canonical]: https://en.wikipedia.org/wiki/Canonical_link_element + +## Actions (astro:actions) + +WIP + +### `defineAction()` + +#### `input` validator + +When using `accept: 'form'`, `input` must use the `z.object()` validator when it is present. The following validators are supported for form data fields: + +- Inputs of type `number` can be validated using `z.number()` +- Inputs of type `checkbox` can be validated using `z.boolean()` +- Inputs of type `file` can be validated using `z.instanceof(File)` +- Multiple inputs of the same `name` can be validated using `z.array(/* validator */)` +- All other inputs can be validated using `z.string()` From 737f4905172301da713e4a4f657107a727eac2ca Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 1 Aug 2024 18:14:01 -0400 Subject: [PATCH 05/66] docs: big form data guide --- src/content/docs/en/guides/actions.mdx | 151 +++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 12 deletions(-) diff --git a/src/content/docs/en/guides/actions.mdx b/src/content/docs/en/guides/actions.mdx index 56a6f276bd86c..0cb5d2865f5b3 100644 --- a/src/content/docs/en/guides/actions.mdx +++ b/src/content/docs/en/guides/actions.mdx @@ -5,11 +5,15 @@ i18nReady: true --- import { Steps } from '@astrojs/starlight/components'; +import ReadMore from '~/components/ReadMore.astro'; +Astro Actions make it easy to define and call backend functions with type-safety. Actions greatly reduce boilerplate on the backend compared to basic API endpoints: -Astro actions make it easy to define and call backend functions with type-safety. +- Automatically validate JSON and form data inputs using [Zod](https://zod.dev). +- Generate type-safe functions to call your backend [from the client](#basic-usage) and even [from server forms](#call-actions-from-an-html-form-action). No need for manual `fetch()` calls. +- Standardize backend errors with the `ActionError` object. -# Basic usage +## Basic usage @@ -23,7 +27,7 @@ Astro actions make it easy to define and call backend functions with type-safety } ``` -3. Import the `defineAction()` utility from `astro:actions`, and the `z` object from `astro:schema`. `defineAction()` accepts a `handler` function containing any backend logic you want to run, and an optional `input` object to validate input parameters with Zod. Anything returned from the `handler` function will be serialized as a response. +3. Import the `defineAction()` utility from `astro:actions`, and the `z` object from `astro:schema`. `defineAction()` accepts a `handler` function containing any backend logic you want to run, and an optional `input` object to validate input parameters with [Zod](https://zod.dev). Anything returned from the `handler` function will be serialized as a response. ```ts ins={1-2, 5-12} title="src/actions/index.ts" import { defineAction } from 'astro:actions'; @@ -41,6 +45,8 @@ Astro actions make it easy to define and call backend functions with type-safety } ``` + The `input` object can validate any JSON-compatible input. [Check the Zod documentation](https://zod.dev/?id=primitives) for all available validation functions. + 4. Call your action from client code by importing `actions` from `astro:actions`. This object contains all functions exported by the `server` object. For example, call `actions.getGreeting()` on a button press using a ` +``` + +## Call actions from an HTML form action + +:::caution +Pages must be server rendered when calling actions using a from action. [Ensure prerendering is disabled on the page](/en/guides/server-side-rendering/#opting-in-to-pre-rendering-in-server-mode) before using this API. +::: + +You may want to call an action using a standard HTML form action. This allows you to submit a form entirely on the server using an Astro component. It is also useful as a fallback for client forms, where slow internet connections or low-powered devices may delay the loading of client-side JavaScript. + +To use a standard form request, add `method="POST"` as a form attribute to any `
` element. Then, apply your action function directly to the `action` property of the form. This will apply the function name as a query string to be handled by the server. + +This example applies the `comment` action to a form using an Astro component: + +```astro title="src/pages/index.astro" ins="action={actions.comment}" ins='method="POST"' +--- +import { actions } from 'astro:actions'; +--- + + + + +