> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/withastro/astro/llms.txt
> Use this file to discover all available pages before exploring further.

# Actions

> Create type-safe server functions for handling form submissions and API calls in Astro

Actions are type-safe server functions that handle form submissions, API calls, and other backend operations. They provide automatic validation, error handling, and progressive enhancement for forms.

## Defining Actions

Create actions in `src/actions/index.ts` by exporting a `server` object:

```typescript src/actions/index.ts theme={null}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  subscribe: defineAction({
    input: z.object({
      email: z.string().email()
    }),
    handler: async ({ email }) => {
      // Your server logic here
      await db.subscribers.create({ email });
      
      return {
        success: true,
        message: 'Subscribed successfully!'
      };
    }
  })
};
```

<Note>
  Actions run exclusively on the server. Client code cannot see or execute handler logic directly.
</Note>

## Using Actions in Forms

Call actions from forms with automatic progressive enhancement:

```astro src/pages/newsletter.astro theme={null}
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.subscribe);
---

<html>
  <body>
    <form method="POST" action={actions.subscribe}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>

    {result?.success && <p>{result.data.message}</p>}
    {result?.error && <p>Error: {result.error.message}</p>}
  </body>
</html>
```

<Tip>
  Forms work without JavaScript but get enhanced with client-side validation and handling when JS is available.
</Tip>

## Input Validation

Use Zod schemas to validate input automatically:

```typescript src/actions/index.ts theme={null}
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  createPost: defineAction({
    input: z.object({
      title: z.string().min(3).max(100),
      content: z.string().min(10),
      tags: z.array(z.string()).optional(),
      published: z.boolean().default(false)
    }),
    handler: async ({ title, content, tags, published }) => {
      // Input is validated and typed
      const post = await db.posts.create({
        title,
        content,
        tags: tags ?? [],
        published
      });

      return { id: post.id };
    }
  })
};
```

### Validation Errors

Invalid input automatically returns typed errors:

```astro src/pages/create-post.astro theme={null}
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.createPost);
---

<form method="POST" action={actions.createPost}>
  <input name="title" required />
  {result?.error?.fields?.title && (
    <p class="error">{result.error.fields.title}</p>
  )}

  <textarea name="content" required></textarea>
  {result?.error?.fields?.content && (
    <p class="error">{result.error.fields.content}</p>
  )}

  <button type="submit">Create Post</button>
</form>
```

## Form Data Handling

For file uploads and complex forms, use `accept: 'form'`:

```typescript src/actions/index.ts theme={null}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  uploadImage: defineAction({
    accept: 'form',
    input: z.object({
      image: z.instanceof(File),
      caption: z.string().optional()
    }),
    handler: async ({ image, caption }) => {
      // Handle file upload
      const buffer = await image.arrayBuffer();
      const url = await storage.upload(buffer, image.name);

      return { url, caption };
    }
  })
};
```

Use in a form:

```astro theme={null}
<form method="POST" action={actions.uploadImage} enctype="multipart/form-data">
  <input type="file" name="image" accept="image/*" required />
  <input type="text" name="caption" placeholder="Caption" />
  <button type="submit">Upload</button>
</form>
```

## Client-Side Actions

Call actions from client-side JavaScript:

```astro src/pages/interactive.astro theme={null}
---
import { actions } from 'astro:actions';
---

<form id="subscribe-form">
  <input type="email" name="email" required />
  <button type="submit">Subscribe</button>
</form>

<div id="result"></div>

<script>
  import { actions } from 'astro:actions';

  const form = document.getElementById('subscribe-form');
  const result = document.getElementById('result');

  form.addEventListener('submit', async (e) => {
    e.preventDefault();

    const formData = new FormData(form);
    const { data, error } = await actions.subscribe(formData);

    if (error) {
      result.textContent = `Error: ${error.message}`;
    } else {
      result.textContent = data.message;
      form.reset();
    }
  });
</script>
```

<Warning>
  Client-side actions require JavaScript. Always provide a fallback form that works without JS.
</Warning>

## Error Handling

Throw `ActionError` for expected errors:

```typescript src/actions/index.ts theme={null}
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  login: defineAction({
    input: z.object({
      email: z.string().email(),
      password: z.string().min(8)
    }),
    handler: async ({ email, password }, context) => {
      const user = await db.users.findByEmail(email);

      if (!user || !await verifyPassword(password, user.passwordHash)) {
        throw new ActionError({
          code: 'UNAUTHORIZED',
          message: 'Invalid email or password'
        });
      }

      // Set session cookie
      context.cookies.set('session', user.sessionId, {
        httpOnly: true,
        secure: true,
        maxAge: 60 * 60 * 24 * 7 // 1 week
      });

      return { user };
    }
  })
};
```

### Error Codes

<ParamField path="BAD_REQUEST" type="400">
  Invalid request format
</ParamField>

<ParamField path="UNAUTHORIZED" type="401">
  Authentication required
</ParamField>

<ParamField path="FORBIDDEN" type="403">
  Insufficient permissions
</ParamField>

<ParamField path="NOT_FOUND" type="404">
  Resource not found
</ParamField>

<ParamField path="TIMEOUT" type="408">
  Request timeout
</ParamField>

<ParamField path="CONFLICT" type="409">
  Resource conflict
</ParamField>

<ParamField path="INTERNAL_SERVER_ERROR" type="500">
  Server error
</ParamField>

## Context Object

Access request context in action handlers:

```typescript src/actions/index.ts theme={null}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  updateProfile: defineAction({
    input: z.object({
      name: z.string()
    }),
    handler: async ({ name }, context) => {
      // Access cookies
      const userId = context.cookies.get('userId')?.value;

      // Access request data
      console.log('User agent:', context.request.headers.get('user-agent'));
      console.log('Client IP:', context.clientAddress);

      // Access shared locals from middleware
      const user = context.locals.user;

      if (!user) {
        throw new ActionError({
          code: 'UNAUTHORIZED',
          message: 'Please log in'
        });
      }

      await db.users.update(user.id, { name });

      return { success: true };
    }
  })
};
```

## JSON Actions

By default, actions accept JSON input:

```typescript src/actions/index.ts theme={null}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  createComment: defineAction({
    // JSON is the default accept type
    input: z.object({
      postId: z.string(),
      content: z.string().min(1).max(1000)
    }),
    handler: async ({ postId, content }, context) => {
      const user = context.locals.user;
      if (!user) {
        throw new ActionError({
          code: 'UNAUTHORIZED',
          message: 'Must be logged in to comment'
        });
      }

      const comment = await db.comments.create({
        postId,
        userId: user.id,
        content
      });

      return comment;
    }
  })
};
```

Call from client code:

```typescript theme={null}
import { actions } from 'astro:actions';

const { data, error } = await actions.createComment({
  postId: '123',
  content: 'Great post!'
});

if (error) {
  console.error(error.message);
} else {
  console.log('Comment created:', data);
}
```

## Progressive Enhancement

Actions work without JavaScript but enhance when available:

```astro src/pages/contact.astro theme={null}
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.contact);
---

<html>
  <body>
    <form method="POST" action={actions.contact} id="contact-form">
      <input name="name" required />
      <input type="email" name="email" required />
      <textarea name="message" required></textarea>
      <button type="submit">Send</button>
    </form>

    {result?.success && <p>Message sent!</p>}
    {result?.error && <p>Error: {result.error.message}</p>}

    <script>
      import { actions } from 'astro:actions';

      const form = document.getElementById('contact-form');

      form?.addEventListener('submit', async (e) => {
        e.preventDefault();
        const formData = new FormData(form);
        const { data, error } = await actions.contact(formData);

        if (error) {
          alert(`Error: ${error.message}`);
        } else {
          alert('Message sent successfully!');
          form.reset();
        }
      });
    </script>
  </body>
</html>
```

<AccordionGroup>
  <Accordion title="Without JavaScript">
    * Form submits via POST request
    * Page reloads with action result
    * Result available via `getActionResult()`
  </Accordion>

  <Accordion title="With JavaScript">
    * Form prevents default submission
    * Action called via client-side JavaScript
    * No page reload required
    * Smooth user experience
  </Accordion>
</AccordionGroup>

## Type Safety

Actions are fully typed:

```typescript theme={null}
import { actions } from 'astro:actions';

// Input is typed from your schema
const result = await actions.createPost({
  title: 'My Post',
  content: 'Content here',
  published: true
});

// Result is typed
if (result.data) {
  console.log(result.data.id); // string
}

if (result.error) {
  console.log(result.error.message); // string
  console.log(result.error.code); // ErrorCode
  console.log(result.error.fields); // Field errors
}
```

## Organizing Actions

For larger projects, organize actions into separate files:

```typescript src/actions/index.ts theme={null}
import { posts } from './posts';
import { users } from './users';
import { comments } from './comments';

export const server = {
  ...posts,
  ...users,
  ...comments
};
```

```typescript src/actions/posts.ts theme={null}
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const posts = {
  createPost: defineAction({
    input: z.object({
      title: z.string(),
      content: z.string()
    }),
    handler: async ({ title, content }) => {
      // Implementation
    }
  }),

  deletePost: defineAction({
    input: z.object({
      id: z.string()
    }),
    handler: async ({ id }) => {
      // Implementation
    }
  })
};
```

## Advanced Patterns

<Tabs>
  <Tab title="Optimistic Updates">
    ```astro theme={null}
    <script>
      import { actions } from 'astro:actions';

      async function likePost(postId: string) {
        // Optimistically update UI
        const button = document.querySelector(`[data-post="${postId}"]`);
        button?.classList.add('liked');

        // Call action
        const { error } = await actions.likePost({ postId });

        // Revert on error
        if (error) {
          button?.classList.remove('liked');
          alert('Failed to like post');
        }
      }
    </script>
    ```
  </Tab>

  <Tab title="Batch Operations">
    ```typescript src/actions/index.ts theme={null}
    import { defineAction } from 'astro:actions';
    import { z } from 'astro:schema';

    export const server = {
      batchDelete: defineAction({
        input: z.object({
          ids: z.array(z.string())
        }),
        handler: async ({ ids }) => {
          await db.posts.deleteMany(ids);
          return { deleted: ids.length };
        }
      })
    };
    ```
  </Tab>

  <Tab title="File Validation">
    ```typescript src/actions/index.ts theme={null}
    import { defineAction, ActionError } from 'astro:actions';
    import { z } from 'astro:schema';

    export const server = {
      uploadAvatar: defineAction({
        accept: 'form',
        input: z.object({
          avatar: z.instanceof(File)
        }),
        handler: async ({ avatar }) => {
          // Validate file type
          if (!avatar.type.startsWith('image/')) {
            throw new ActionError({
              code: 'BAD_REQUEST',
              message: 'File must be an image'
            });
          }

          // Validate file size (5MB)
          if (avatar.size > 5 * 1024 * 1024) {
            throw new ActionError({
              code: 'BAD_REQUEST',
              message: 'File must be under 5MB'
            });
          }

          // Process upload
          const url = await uploadFile(avatar);
          return { url };
        }
      })
    };
    ```
  </Tab>
</Tabs>

## Related Resources

<CardGroup cols={2}>
  <Card title="Middleware" icon="filter" href="/features/middleware">
    Add authentication and shared logic
  </Card>

  <Card title="API Routes" icon="code" href="/concepts/routing">
    Build custom API endpoints
  </Card>

  <Card title="SSR" icon="server" href="/features/ssr-and-ssg">
    Server-side rendering
  </Card>

  <Card title="Type Safety" icon="shield" href="/config/typescript">
    TypeScript configuration
  </Card>
</CardGroup>
