Skip to content

Actions – astrojs-wp-integration

Actions are Astro server actions that wrap the WordPress REST API, providing type-safe authenticated operations from any server-side Astro context.

Every action factory works directly with a WordPressClient or a request-aware client resolver such as wordPressAuthBridge.getClient. Under the hood, each action delegates to fluent-wp-client and maps failures to ActionError.

Action integration tests run through a real Astro fixture app and call /_actions/* endpoints over HTTP. This validates routing, input parsing, handler execution, and serialized responses as one end-to-end flow.

Use npm run test:integration for action/loaders/auth integration checks through the real Astro fixture app.


All write operations use one WordPressClient or one request-aware client resolver.

import {
createCreatePostAction,
createCreateTermAction,
createCreateUserAction,
createDeletePostAction,
createGetAbilityAction,
createUpdatePostAction,
} from 'wp-astrojs-integration';
import { wordPressAuthBridge } from '../lib/auth/bridge';
export const server = {
createPost: createCreatePostAction(wordPressAuthBridge.getClient),
updatePost: createUpdatePostAction(wordPressAuthBridge.getClient),
deletePost: createDeletePostAction(wordPressAuthBridge.getClient),
createCategory: createCreateTermAction(wordPressAuthBridge.getClient, { resource: 'categories' }),
createUser: createCreateUserAction(wordPressAuthBridge.getClient),
getAbility: createGetAbilityAction(wordPressAuthBridge.getClient),
};
import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({
baseUrl: import.meta.env.WP_URL,
auth: {
username: import.meta.env.WP_USERNAME,
password: import.meta.env.WP_APP_PASSWORD,
},
});
export const server = {
createPost: createCreatePostAction(wp),
updatePost: createUpdatePostAction(wp),
};
import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({
baseUrl: import.meta.env.WP_URL,
auth: {
token: import.meta.env.WP_JWT_TOKEN,
},
});

Static client with request-aware auth headers

Section titled “Static client with request-aware auth headers”
import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({
baseUrl: import.meta.env.WP_URL,
authHeaders: ({ method, url, body }) => {
// Replace with your signer implementation
const signature = createSignedAuthHeader({ method, url: url.toString(), body });
return {
Authorization: signature,
};
},
});

createSignedAuthHeader is a user-defined function for custom auth integrations. It is not part of this package and does not perform a network request by itself.

Use the same client in all factories when you want one shared static auth setup:

src/actions/index.ts
import {
createCreatePostAction,
createUpdatePostAction,
createDeletePostAction,
createCreateTermAction,
createUpdateTermAction,
createDeleteTermAction,
createCreateUserAction,
createUpdateUserAction,
createDeleteUserAction,
createGetAbilityAction,
createRunAbilityAction,
} from 'wp-astrojs-integration';
import { WordPressClient } from 'fluent-wp-client';
const wp = new WordPressClient({
baseUrl: import.meta.env.WP_URL,
auth: {
username: import.meta.env.WP_USERNAME,
password: import.meta.env.WP_APP_PASSWORD,
},
});
export const server = {
createPost: createCreatePostAction(wp),
updatePost: createUpdatePostAction(wp),
deletePost: createDeletePostAction(wp),
createCategory: createCreateTermAction(wp, { resource: 'categories' }),
updateTag: createUpdateTermAction(wp, { resource: 'tags' }),
deleteGenre: createDeleteTermAction(wp, { resource: 'genres' }),
createUser: createCreateUserAction(wp),
updateUser: createUpdateUserAction(wp),
deleteUser: createDeleteUserAction(wp),
getAbility: createGetAbilityAction(wp),
runAbility: createRunAbilityAction(wp),
deleteAbility: createDeleteAbilityAction(wp),
};

Input validation happens in two distinct layers. Understanding where each field is checked helps you handle errors correctly.

Layer 1 — Zod (local, before the HTTP request)

Section titled “Layer 1 — Zod (local, before the HTTP request)”

These constraints are enforced by the Zod schema before any request reaches WordPress. A violation throws immediately in the Astro server process and never touches the network.

FieldConstraint
id (update / delete)Must be a positive integer
force (delete)Must be a boolean
statusMust be one of 'publish', 'draft', 'pending', 'private', 'future'
comment_statusMust be a string (type only — not all post types support comments)
ping_statusMust be a string (type only — not all post types support pingbacks)
formatMust be a string (type only — only the built-in post type supports formats)
stickyMust be a boolean
passwordMust be a string
authorMust be an integer (type only — existence is not checked)
featured_mediaMust be an integer (type only — existence is not checked)
categoriesMust be an array of integers (type only — term existence is not checked)
tagsMust be an array of integers (type only — term existence is not checked)
parentMust be an integer (type only — used by hierarchical types like pages)
menu_orderMust be an integer (type only — used by hierarchical types like pages)
title, content, excerptMust be strings
date, date_gmtMust be strings (format is not validated — see Layer 2)
slug, templateMust be strings (content is not validated — see Layer 2)
metaMust be a plain object (keys and value types are not validated)

Layer 2 — WordPress REST API (remote, after the HTTP request)

Section titled “Layer 2 — WordPress REST API (remote, after the HTTP request)”

These fields are forwarded to WordPress without further local inspection. WordPress validates them server-side and returns a structured error if they are invalid. The action catches that response and surfaces it as an ActionError.

FieldWhat WordPress checks
authorThe user ID must exist and the authenticated user must be allowed to assign that author
featured_mediaThe attachment ID must exist and be of media type image
categoriesEach ID must correspond to a published category term
tagsEach ID must correspond to a published tag term
date / date_gmtThe string must be a valid ISO 8601 date-time
slugAccepted as-is; WordPress appends a suffix (e.g. -2) if it conflicts with an existing post
templateThe filename must match a template registered by the active theme
meta keysOnly keys registered with show_in_rest: true are accepted; unregistered keys are silently ignored

The base schema uses .passthrough(), meaning any extra key you include is forwarded to WordPress without Zod touching it. This is intentional — it allows ACF fields, custom REST fields, and other plugin data to pass through end-to-end.

Extend the schema with .extend() if you want Zod to validate those extra fields locally too:

import { createPostInputSchema } from 'wp-astrojs-integration';
import { z } from 'astro/zod';
const mySchema = createPostInputSchema.extend({
acf: z.object({
price: z.number().positive(),
sku: z.string(),
}).optional(),
});

Use mapResponse on create/update post actions when the successful WordPress response needs site-specific normalization before Astro receives it, such as replacing ACF choice values with labels from discovery metadata.

See Mapping WordPress Values for a reusable callback pattern that works with both loaders and actions.


All actions surface failures as ActionError. Use the error field returned by Astro.callAction:

const { data, error } = await Astro.callAction(actions.createPost, {
title: 'My Post',
status: 'publish',
});
if (error) {
// error.code — HTTP status mapped to an Astro error code string
// error.message — original WordPress error message, passed through verbatim
console.error(error.code, error.message);
}

The message is taken directly from the WordPress { code, message, data } error response and not modified. This means it reflects exactly what WordPress rejected and why.

error.codeHTTP statusTypical cause
UNAUTHORIZED401Missing, wrong, or expired credentials (basic auth or JWT)
FORBIDDEN403Authenticated user lacks capability for the operation
NOT_FOUND404Post / term / attachment ID does not exist
UNPROCESSABLE_CONTENT422Invalid field value (bad date format, unknown template, etc.)
BAD_REQUEST400Malformed request body
INTERNAL_SERVER_ERROR500WordPress or PHP error on the server

Zod validation failures (Layer 1) are surfaced as BAD_REQUEST before the request is sent.


Every action file exports two things:

  • create*Action(client) — the Astro ActionClient factory for use in src/actions/index.ts.
  • execute*(client, input, options?) — the raw async helper, useful for testing or building custom actions that target a different REST resource (e.g. a custom post type).
import { WordPressClient } from 'fluent-wp-client';
import { executeCreatePost } from 'wp-astrojs-integration/actions';
const wp = new WordPressClient({
baseUrl: 'https://example.com',
auth: { username, password },
});
const product = await executeCreatePost(
wp,
{ title: 'My Product', status: 'publish' },
{ resource: 'products' },
);

If you use JWT, build the client with auth: { token } instead.

When resource is omitted it defaults to 'posts' for content actions and 'categories' for term actions.

This is also how integration tests call actions directly — without needing the Astro runtime.

  • TypeScript88%
  • PHP11%
  • Astro1%