CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-astro

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

server-actions.mddocs/

Server Actions

Server Actions provide a type-safe way to define and call server-side functions from client code. Actions handle form submissions, validate input with Zod schemas, and provide automatic error handling with type-safe results.

Capabilities

Define Action

Defines a server-side action with input validation and type-safe handling.

/**
 * Defines a server-side action
 * @param config - Action configuration
 * @returns Action client function with type-safe input/output
 */
function defineAction<
  TOutput,
  TAccept extends ActionAccept | undefined = undefined,
  TInputSchema extends z.ZodType | undefined = undefined
>(config: {
  /** Input validation schema (Zod) */
  input?: TInputSchema;
  /** Accept type: 'form' for FormData or 'json' for JSON */
  accept?: TAccept;
  /** Handler function receiving validated input and context */
  handler: ActionHandler<TInputSchema, TOutput>;
}): ActionClient<TOutput, TAccept, TInputSchema> & string;

type ActionAccept = 'form' | 'json';

type ActionHandler<TInputSchema, TOutput> =
  TInputSchema extends z.ZodType
    ? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput>
    : (input: any, context: ActionAPIContext) => MaybePromise<TOutput>;

Usage Examples:

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

// Form action with validation
export const server = {
  newsletter: defineAction({
    accept: 'form',
    input: z.object({
      email: z.string().email(),
      name: z.string().min(2),
    }),
    handler: async (input, context) => {
      // input is automatically validated and typed
      await saveToDatabase(input.email, input.name);
      return { success: true };
    },
  }),

  // JSON action
  updateProfile: defineAction({
    accept: 'json',
    input: z.object({
      userId: z.string(),
      bio: z.string().max(500),
    }),
    handler: async (input, context) => {
      const user = await updateUser(input.userId, { bio: input.bio });
      return { user };
    },
  }),

  // Action without input validation
  getCurrentUser: defineAction({
    handler: async (input, context) => {
      const userId = context.cookies.get('userId');
      return await fetchUser(userId);
    },
  }),
};

Client-side usage:

---
import { actions } from 'astro:actions';
---

<!-- Form action -->
<form method="POST" action={actions.newsletter}>
  <input type="email" name="email" required />
  <input type="text" name="name" required />
  <button>Subscribe</button>
</form>

<!-- Programmatic call -->
<script>
  import { actions } from 'astro:actions';

  const result = await actions.updateProfile({
    userId: '123',
    bio: 'Hello world',
  });

  if (result.error) {
    console.error(result.error);
  } else {
    console.log(result.data);
  }
</script>

Get Action Context

Accesses action request information from middleware.

/**
 * Gets action context from middleware
 * Provides information about action requests and result handling
 * @param context - API context from middleware
 * @returns Action context with action info and serialization utilities
 */
function getActionContext(context: APIContext): AstroActionContext;

interface AstroActionContext {
  /** Information about the incoming action request (undefined if not an action) */
  action?: {
    /** How the action was called: 'rpc' (programmatic) or 'form' (HTML form) */
    calledFrom: 'rpc' | 'form';
    /** Name of the action being called */
    name: string;
    /** Function to programmatically call the action and get the result */
    handler: () => Promise<SafeResult<any, any>>;
  };

  /**
   * Manually set the action result for templates
   * Disables Astro's automatic result handling
   */
  setActionResult(actionName: string, actionResult: SerializedActionResult): void;

  /** Serialize an action result for storage in cookies or sessions */
  serializeActionResult: typeof serializeActionResult;

  /** Deserialize an action result to access data and error objects */
  deserializeActionResult: typeof deserializeActionResult;
}
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';

export const onRequest = defineMiddleware(async (context, next) => {
  const actionContext = getActionContext(context);

  if (actionContext.action) {
    console.log(`Action called: ${actionContext.action.name}`);
    console.log(`Called from: ${actionContext.action.calledFrom}`);

    // Optionally handle the action manually
    const result = await actionContext.action.handler();
    if (result.error) {
      // Custom error handling
      return new Response('Action failed', { status: 500 });
    }

    // Set custom action result
    const serialized = actionContext.serializeActionResult(result);
    actionContext.setActionResult(actionContext.action.name, serialized);
  }

  return next();
});

Form Data To Object

Converts FormData to a JavaScript object based on a Zod schema.

/**
 * Transforms FormData to an object based on Zod schema
 * Handles type coercion for numbers, booleans, and arrays
 * @param formData - FormData to convert
 * @param schema - Zod object schema defining the structure
 * @returns Object with typed values
 */
function formDataToObject<T extends z.AnyZodObject>(
  formData: FormData,
  schema: T
): Record<string, unknown>;
import { formDataToObject } from 'astro:actions';
import { z } from 'astro:content';

const schema = z.object({
  name: z.string(),
  age: z.number(),
  active: z.boolean(),
  tags: z.array(z.string()),
});

const formData = new FormData();
formData.append('name', 'Alice');
formData.append('age', '25');
formData.append('active', 'true');
formData.append('tags', 'javascript');
formData.append('tags', 'typescript');

const obj = formDataToObject(formData, schema);
// { name: 'Alice', age: 25, active: true, tags: ['javascript', 'typescript'] }

Error Handling

Action Error

Base error class for action errors with HTTP status codes.

/**
 * Error class for action failures
 * Includes HTTP status code and type information
 */
class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
  type: 'AstroActionError';
  code: ActionErrorCode;
  status: number;

  constructor(params: {
    /** Error message */
    message?: string;
    /** HTTP-based error code */
    code: ActionErrorCode;
    /** Optional stack trace */
    stack?: string;
  });

  /** Convert error code to HTTP status */
  static codeToStatus(code: ActionErrorCode): number;

  /** Convert HTTP status to error code */
  static statusToCode(status: number): ActionErrorCode;

  /** Create error from JSON response */
  static fromJson(body: any): ActionError;
}

/** HTTP-based error codes (e.g., 'BAD_REQUEST', 'UNAUTHORIZED', etc.) */
type ActionErrorCode =
  | 'BAD_REQUEST'
  | 'UNAUTHORIZED'
  | 'PAYMENT_REQUIRED'
  | 'FORBIDDEN'
  | 'NOT_FOUND'
  | 'METHOD_NOT_ALLOWED'
  | 'NOT_ACCEPTABLE'
  | 'PROXY_AUTHENTICATION_REQUIRED'
  | 'REQUEST_TIMEOUT'
  | 'CONFLICT'
  | 'GONE'
  | 'LENGTH_REQUIRED'
  | 'PRECONDITION_FAILED'
  | 'CONTENT_TOO_LARGE'
  | 'URI_TOO_LONG'
  | 'UNSUPPORTED_MEDIA_TYPE'
  | 'RANGE_NOT_SATISFIABLE'
  | 'EXPECTATION_FAILED'
  | 'MISDIRECTED_REQUEST'
  | 'UNPROCESSABLE_CONTENT'
  | 'LOCKED'
  | 'FAILED_DEPENDENCY'
  | 'TOO_EARLY'
  | 'UPGRADE_REQUIRED'
  | 'PRECONDITION_REQUIRED'
  | 'TOO_MANY_REQUESTS'
  | 'REQUEST_HEADER_FIELDS_TOO_LARGE'
  | 'UNAVAILABLE_FOR_LEGAL_REASONS'
  | 'INTERNAL_SERVER_ERROR'
  | 'NOT_IMPLEMENTED'
  | 'BAD_GATEWAY'
  | 'SERVICE_UNAVAILABLE'
  | 'GATEWAY_TIMEOUT'
  | 'HTTP_VERSION_NOT_SUPPORTED'
  | 'VARIANT_ALSO_NEGOTIATES'
  | 'INSUFFICIENT_STORAGE'
  | 'LOOP_DETECTED'
  | 'NETWORK_AUTHENTICATION_REQUIRED';

/** Array of all valid action error codes */
const ACTION_ERROR_CODES: readonly ActionErrorCode[];
import { defineAction, ActionError } from 'astro:actions';

export const server = {
  deletePost: defineAction({
    input: z.object({ postId: z.string() }),
    handler: async (input, context) => {
      const user = context.locals.user;
      if (!user) {
        throw new ActionError({
          code: 'UNAUTHORIZED',
          message: 'You must be logged in',
        });
      }

      const post = await getPost(input.postId);
      if (!post) {
        throw new ActionError({
          code: 'NOT_FOUND',
          message: 'Post not found',
        });
      }

      if (post.authorId !== user.id) {
        throw new ActionError({
          code: 'FORBIDDEN',
          message: 'You can only delete your own posts',
        });
      }

      await deletePost(input.postId);
      return { success: true };
    },
  }),
};

Action Input Error

Error class for input validation failures (Zod schema validation).

/**
 * Error class for input validation failures
 * Extends ActionError with Zod validation issues
 */
class ActionInputError<T extends ErrorInferenceObject> extends ActionError {
  type: 'AstroActionInputError';

  /** Array of Zod validation issues */
  issues: z.ZodIssue[];

  /** Field-specific error messages */
  fields: Record<string, string[]>;

  constructor(issues: z.ZodIssue[]);
}
<script>
  import { actions, isInputError } from 'astro:actions';

  const result = await actions.newsletter({
    email: 'invalid-email',
    name: 'A',
  });

  if (result.error) {
    if (isInputError(result.error)) {
      // Validation error
      console.log(result.error.fields.email); // ["Invalid email"]
      console.log(result.error.fields.name); // ["String must contain at least 2 characters"]
      console.log(result.error.issues); // Full Zod issues array
    } else {
      // Other action error
      console.log(result.error.message);
    }
  }
</script>

Is Action Error

Type guard to check if an error is an ActionError.

/**
 * Type guard for ActionError
 * @param error - Error to check
 * @returns True if error is an ActionError (includes ActionInputError)
 */
function isActionError(error?: unknown): error is ActionError;
import { actions, isActionError } from 'astro:actions';

try {
  const result = await actions.updateProfile({ bio: 'New bio' });
} catch (error) {
  if (isActionError(error)) {
    // error is ActionError or ActionInputError
    console.log('Action failed:', error.code, error.message);
  } else {
    // Unknown error type
    console.error('Unexpected error:', error);
  }
}

Is Input Error

Type guard to check if an error is an ActionInputError specifically.

/**
 * Type guard for ActionInputError (input validation errors)
 * @param error - Error to check
 * @returns True if error is ActionInputError
 */
function isInputError<T extends ErrorInferenceObject>(
  error?: ActionError<T>
): error is ActionInputError<T>;
import { actions, isInputError } from 'astro:actions';

const result = await actions.newsletter.safe({ email: 'invalid' });

if (!result.data) {
  if (isInputError(result.error)) {
    // Validation error - show field-specific errors
    console.log('Validation failed:', result.error.fields);
  } else {
    // Other action error
    console.log('Action error:', result.error.message);
  }
}

Serialization Utilities

Serialize Action Result

Serializes an action result for storage or transmission.

/**
 * Serializes action result to a storable format
 * Used internally and for custom result handling in middleware
 * @param result - Safe result with data or error
 * @returns Serialized result
 */
function serializeActionResult(
  result: SafeResult<any, any>
): SerializedActionResult;

type SerializedActionResult =
  | {
      type: 'data';
      contentType: 'application/json+devalue';
      status: 200;
      body: string;
    }
  | {
      type: 'error';
      contentType: 'application/json';
      status: number;
      body: string;
    }
  | {
      type: 'empty';
      status: 204;
    };

Deserialize Action Result

Deserializes an action result from storage or transmission.

/**
 * Deserializes action result from serialized format
 * @param result - Serialized action result
 * @returns Safe result with data or error
 */
function deserializeActionResult(
  result: SerializedActionResult
): SafeResult<any, any>;

Call Safely

Wraps a handler call with automatic error handling.

/**
 * Executes a handler with automatic error catching
 * Converts thrown errors to ActionError instances
 * @param handler - Handler function to execute
 * @returns Safe result with data or error
 */
function callSafely<TOutput>(
  handler: () => MaybePromise<TOutput>
): Promise<SafeResult<z.ZodType, TOutput>>;

Types

Safe Result

Type-safe result wrapper for action responses.

/**
 * Type-safe result wrapper
 * Either contains data or error, never both
 */
type SafeResult<TInput extends ErrorInferenceObject, TOutput> =
  | {
      data: TOutput;
      error: undefined;
    }
  | {
      data: undefined;
      error: ActionError<TInput>;
    };

Action Client

Type-safe client interface for calling actions.

/**
 * Client-side action caller with type safety
 * Provides both safe calls (returns SafeResult) and orThrow variant
 */
type ActionClient<
  TOutput,
  TAccept extends ActionAccept | undefined,
  TInputSchema extends z.ZodType | undefined
> = ((input: /* inferred from schema */) => Promise<SafeResult</* inferred */, TOutput>>) & {
  /** Query string for form actions */
  queryString: string;
  /** Throws errors instead of returning SafeResult */
  orThrow: (input: /* inferred from schema */) => Promise<TOutput>;
};

Action API Context

Context object passed to action handlers.

/**
 * Context passed to action handlers
 * Extends APIContext with action-specific properties
 */
type ActionAPIContext = Omit<APIContext, 'props' | 'getActionResult' | 'callAction' | 'redirect'>;

Action Return Type

Utility type to extract return type from action handler.

/**
 * Extracts return type from action handler
 * Useful for typing action results
 */
type ActionReturnType<T extends ActionHandler<any, any>> = Awaited<ReturnType<T>>;

Action Runtime Utilities

Actions Proxy

The actions proxy provides access to all defined server actions with type safety.

/**
 * Proxy object for accessing defined actions
 * Provides type-safe access to all actions defined in src/actions/index.ts
 */
const actions: ActionProxy;
import { actions } from 'astro:actions';

// Call actions programmatically
const result = await actions.newsletter({
  email: 'user@example.com',
  name: 'John Doe',
});

// Use with forms
<form method="POST" action={actions.newsletter}>
  <input type="email" name="email" />
  <button>Subscribe</button>
</form>

// Access nested actions
const result = await actions.user.profile.update({ bio: 'Hello' });

// Throw on error instead of returning SafeResult
const data = await actions.newsletter.orThrow({
  email: 'user@example.com',
  name: 'John Doe',
});

Get Action Path

Gets the URL path for an action, useful for manual fetch requests or custom integrations.

/**
 * Gets the URL path for an action
 * @param action - Action client function
 * @returns Action URL path including base URL
 */
function getActionPath(action: ActionClient<any, any, any>): string;
import { actions, getActionPath } from 'astro:actions';

// Get the action's URL path
const path = getActionPath(actions.newsletter);
// Returns: '/_actions/newsletter' (with base URL and trailing slash if configured)

// Use for custom fetch
const response = await fetch(path, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'user@example.com', name: 'John' }),
});

Get Action Query String

Generates a query string for a named action, useful for constructing action URLs manually.

/**
 * Generates query string for an action by name
 * @param name - Action name (e.g., 'newsletter' or 'user.profile.update')
 * @returns Query string with action parameter (e.g., '?_astroAction=newsletter')
 */
function getActionQueryString(name: string): string;
import { getActionQueryString } from 'astro:actions';

// Get query string for action
const qs = getActionQueryString('newsletter');
// Returns: '?_astroAction=newsletter'

// Construct full action URL
const actionUrl = `/_actions/newsletter${qs}`;

// Useful for custom routing or integration scenarios
const response = await fetch(actionUrl, {
  method: 'POST',
  body: formData,
});

Virtual Module

All server action APIs are available from the astro:actions virtual module:

import {
  actions,
  defineAction,
  getActionContext,
  getActionPath,
  formDataToObject,
  ActionError,
  ActionInputError,
  isInputError,
  callSafely,
  serializeActionResult,
  deserializeActionResult,
  type ActionAccept,
  type ActionHandler,
  type ActionClient,
  type ActionErrorCode,
  type SafeResult,
  type SerializedActionResult,
  type AstroActionContext,
  type ActionAPIContext,
  type ActionReturnType,
} from 'astro:actions';

The astro/actions/runtime/server and astro/actions/runtime/client modules also export these same APIs but are typically not imported directly.

docs

assets.md

cli-and-build.md

configuration.md

container.md

content-collections.md

content-loaders.md

dev-toolbar.md

environment.md

i18n.md

index.md

integrations.md

middleware.md

server-actions.md

ssr-and-app.md

transitions.md

tile.json