DEV Community

Alex Spinov
Alex Spinov

Posted on

Astro Actions Has a Free API: Type-Safe Server Functions with Built-in Validation

Why Astro Actions?

Astro Actions let you define type-safe server functions that you call directly from your client code. Think tRPC but built into the framework - with Zod validation, automatic error handling, and zero boilerplate.

Define an Action

// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  createPost: defineAction({
    accept: 'json',
    input: z.object({
      title: z.string().min(1).max(200),
      body: z.string().min(10),
      tags: z.array(z.string()).optional(),
    }),
    handler: async (input, context) => {
      const post = await db.posts.create({
        data: { ...input, authorId: context.locals.userId },
      });
      return { id: post.id, slug: post.slug };
    },
  }),

  deletePost: defineAction({
    accept: 'json',
    input: z.object({ id: z.string().uuid() }),
    handler: async ({ id }) => {
      await db.posts.delete({ where: { id } });
      return { success: true };
    },
  }),
};
Enter fullscreen mode Exit fullscreen mode

Call from Client Components

import { actions } from 'astro:actions';

export default function PostForm() {
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const { data, error } = await actions.createPost({
      title: formData.get('title') as string,
      body: formData.get('body') as string,
    });
    if (error) {
      console.error('Validation errors:', error.fields);
      return;
    }
    window.location.href = `/posts/${data.slug}`;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" placeholder="Post title" required />
      <textarea name="body" placeholder="Write your post..." />
      <button type="submit">Publish</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Form Actions (Progressive Enhancement)

---
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.submitContact);
if (result && !result.error) {
  return Astro.redirect('/thank-you');
}
---

<form method="POST" action={actions.submitContact}>
  <input name="email" type="email" required />
  <textarea name="message" required />
  {result?.error && <p class="error">{result.error.message}</p>}
  <button type="submit">Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Error Handling

import { ActionError, defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  subscribe: defineAction({
    input: z.object({ email: z.string().email() }),
    handler: async ({ email }) => {
      const existing = await db.subscribers.findUnique({ where: { email } });
      if (existing) {
        throw new ActionError({ code: 'CONFLICT', message: 'Already subscribed' });
      }
      await db.subscribers.create({ data: { email } });
      return { subscribed: true };
    },
  }),
};
Enter fullscreen mode Exit fullscreen mode

File Uploads

export const server = {
  uploadAvatar: defineAction({
    accept: 'form',
    input: z.object({
      avatar: z.instanceof(File)
        .refine(f => f.size < 5_000_000, 'Max 5MB')
        .refine(f => f.type.startsWith('image/'), 'Must be an image'),
    }),
    handler: async ({ avatar }, context) => {
      const buffer = await avatar.arrayBuffer();
      const path = `avatars/${context.locals.userId}.webp`;
      await storage.upload(path, buffer);
      return { url: storage.getPublicUrl(path) };
    },
  }),
};
Enter fullscreen mode Exit fullscreen mode

Real-World Use Case

A developer building a content site on Astro needed form handling without spinning up a separate API. With Actions, they defined 5 server functions in one file - contact form, newsletter signup, comment submission, like button, and search. Full type safety from form to database, Zod validation catches bad input before it hits the handler.


Building with Astro? I create custom data pipelines and automation tools. Check out my web scraping toolkit on Apify or reach me at spinov001@gmail.com for custom solutions.

Top comments (0)