Claude Code Plugins

Community-maintained marketplace

Feedback

Write server actions following the Epic architecture patterns. Use when creating server-side logic for behaviors, including authentication, validation, and model calls. Triggers on "create an action", "add an action", or "write an action for".

Install Skill

Shared

Installs to .agents/skills, used by Codex, Amp, Warp, Cursor, OpenCode, and more.

CodexAmp
Warp
CursorOpenCode
Cline
Gemini CLI
GitHub Copilot
Personal

Available across projects.

$npx skills-installer add @majiayu000/claude-skill-registry/write-action --client shared
Project

Writes to .agents/skills.

$npx skills-installer add @majiayu000/claude-skill-registry/write-action -p --client shared
Note: Review the skill instructions before using it.

SKILL.md

name write-action
description Write server actions following the Epic architecture patterns. Use when creating server-side logic for behaviors, including authentication, validation, and model calls. Triggers on "create an action", "add an action", or "write an action for".

Write Action

Overview

This skill creates server actions that follow the Epic three-layer architecture. Actions belong to the Backend layer and handle authentication, validation, and orchestration of model calls.

Architecture Context

Frontend: Hooks call actions
            |
            v
Backend: Actions (auth + validation + orchestration)
            |
            v
Infrastructure: Models (database operations)

Actions:

  • Run on the server (Backend layer)
  • Check authentication via getUser()
  • Validate inputs with Zod
  • Call models for data operations
  • Return consistent response format
  • NEVER access database directly

Action Location and Naming

app/[role]/[page]/behaviors/[behavior-name]/
  actions/
    [action-name].action.ts
  • File names: kebab-case.action.ts
  • Function names: camelCase

Function Specification Format

Follow the Epic Function specification format from docs/Epic.md:

## functionName(input: InputType): ReturnType

[Short description of what the function does]

- Given: [input parameters and assumptions]
- Returns: [value or outcome returned]
- Calls: [direct dependencies - models, integrations]

### Example: [Scenario name]

#### PreDB
[table_name]:
column1, column2
value1, value2

#### PostDB
[table_name]:
column1, column2
value1, value2
new_id, new_val

Implementation Pattern

'use server';

import { getUser } from '@/lib/auth';
import { Model } from '@/shared/models/model-name';
import { z } from 'zod';

const InputSchema = z.object({
  name: z.string().min(1).max(100),
});

type Input = z.infer<typeof InputSchema>;

export async function actionName(input: Input) {
  try {
    // 1. Authentication check
    const user = await getUser();
    if (!user) {
      return { success: false, error: 'Unauthorized' };
    }

    // 2. Validate input
    const validated = InputSchema.parse(input);

    // 3. Call model (never direct DB access)
    const result = await Model.create({
      ...validated,
      userId: user.id,
    });

    // 4. Return success response
    return { success: true, data: result };
  } catch (error) {
    console.error('actionName error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'An error occurred',
    };
  }
}

Response Format

Always return consistent format:

type ActionResponse<T> =
  | { success: true; data: T }
  | { success: false; error: string };

Key Patterns

1. Authentication First

const user = await getUser();
if (!user) {
  return { success: false, error: 'Unauthorized' };
}

2. Input Validation

const validated = InputSchema.parse(input);
// or with safeParse for custom error handling
const result = InputSchema.safeParse(input);
if (!result.success) {
  return { success: false, error: result.error.errors[0].message };
}

3. User-Scoped Operations

// Always filter by userId for user-owned resources
const items = await Model.findByUserId(user.id);

4. Error Handling

try {
  // operation
} catch (error) {
  console.error('actionName error:', error);
  return {
    success: false,
    error: error instanceof Error ? error.message : 'An error occurred',
  };
}

Constraints

  • MUST include 'use server' directive at top
  • MUST check authentication when required
  • NEVER access database directly - use models
  • NEVER import React, Jotai, or frontend code
  • ALWAYS return consistent response format
  • ALWAYS use try/catch with descriptive errors

Example Specification

## createProject(input: CreateProjectInput): Promise<ActionResponse<Project>>

Creates a new project for the authenticated user.

- Given: project name (1-100 chars) and authenticated user with "client" role
- Returns: the newly created project with status "draft"
- Calls: ProjectModel.findByNameAndUser, ProjectModel.create

### Example: Create project successfully

#### PreDB
users:
id, email, role
1, user@example.com, client

projects:
id, user_id, name, status
1, 1, Existing Project, active

#### Steps
* Call: createProject({ name: "New Project" }) as user 1
* Returns: { id: 2, name: "New Project", status: "draft" }

#### PostDB
projects:
id, user_id, name, status
1, 1, Existing Project, active
2, 1, New Project, draft

### Example: Reject duplicate name

#### PreDB
projects:
id, user_id, name
1, 1, My Project

#### Steps
* Call: createProject({ name: "My Project" }) as user 1
* Throws: "Project name already exists"

#### PostDB
projects:
id, user_id, name
1, 1, My Project

Test Generation

Generate test files at [behavior-path]/tests/[action-name].action.test.ts.

Test Structure

import { describe, it, expect } from 'vitest';
import { PreDB, PostDB } from '@/lib/db-test';
import { db } from '@/db';
import * as schema from '@/db/schema';
import { actionName } from '../[action-name].action';

describe('actionName', () => {
  it('should [behavior] when [condition]', async () => {
    // PreDB -> PreDB
    await PreDB(db, schema, {
      users: [{ id: '1', email: 'user@example.com' }],
      projects: [],
    });

    // Steps -> Execute
    const result = await actionName({ name: 'New Project' });

    // Returns -> Assertions
    expect(result.success).toBe(true);
    expect(result.data?.name).toBe('New Project');

    // PostDB -> PostDB
    await PostDB(db, schema, {
      projects: [{ id: result.data?.id, name: 'New Project', status: 'draft' }],
    }, { allowExtraRows: true });
  });
});

Translation Rules

Spec Test
PreDB (CSV) PreDB(db, schema, { table: [...] })
Call: Action invocation
Returns: expect(result).toBe(...)
Throws: expect(result.error).toBe(...)
PostDB (CSV) PostDB(db, schema, { table: [...] })

Principles

  • Test behavior, not implementation
  • Use real database (no mocks)
  • Start with ONE test (happy path)