Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
4.5 KiB
name, description
| name | description |
|---|---|
| server-action-builder | Create Next.js Server Actions with enhanceAction, Zod validation, and service patterns. Use when implementing mutations, form submissions, or API operations that need authentication and validation. Invoke with /server-action-builder. |
Server Action Builder
You are an expert at creating type-safe server actions for Makerkit following established patterns.
Workflow
When asked to create a server action, follow these steps:
Step 1: Create Zod Schema
Create validation schema in _lib/schemas/:
// _lib/schemas/feature.schema.ts
import * as z from 'zod';
export const CreateFeatureSchema = z.object({
name: z.string().min(1, 'Name is required'),
accountId: z.string().uuid('Invalid account ID'),
});
export type CreateFeatureInput = z.output<typeof CreateFeatureSchema>;
Step 2: Create Service Layer
North star: services are decoupled from their interface. The service is pure logic — it receives a database client as a dependency, never imports one. This means the same service works whether called from a server action, an MCP tool, a CLI command, or a plain unit test.
Create service in _lib/server/:
// _lib/server/feature.service.ts
import type { SupabaseClient } from '@supabase/supabase-js';
import type { CreateFeatureInput } from '../schemas/feature.schema';
export function createFeatureService(client: SupabaseClient) {
return new FeatureService(client);
}
class FeatureService {
constructor(private readonly client: SupabaseClient) {}
async create(data: CreateFeatureInput) {
const { data: result, error } = await this.client
.from('features')
.insert({
name: data.name,
account_id: data.accountId,
})
.select()
.single();
if (error) throw error;
return result;
}
}
The service never calls getSupabaseServerClient() — the caller provides the client. This keeps the service testable (
pass a mock client) and reusable (any interface can supply its own client).
Step 3: Create Server Action (Thin Adapter)
The action is a thin adapter — it resolves dependencies (client, logger) and delegates to the service. No business logic lives here.
Create action in _lib/server/server-actions.ts:
'use server';
import { enhanceAction } from '@kit/next/actions';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { revalidatePath } from 'next/cache';
import { CreateFeatureSchema } from '../schemas/feature.schema';
import { createFeatureService } from './feature.service';
export const createFeatureAction = enhanceAction(
async function (data, user) {
const logger = await getLogger();
const ctx = { name: 'create-feature', userId: user.id };
logger.info(ctx, 'Creating feature');
const client = getSupabaseServerClient();
const service = createFeatureService(client);
const result = await service.create(data);
logger.info({ ...ctx, featureId: result.id }, 'Feature created');
revalidatePath('/home/[account]/features');
return { success: true, data: result };
},
{
auth: true,
schema: CreateFeatureSchema,
},
);
Key Patterns
- Services are pure, interfaces are thin adapters. The service contains all business logic. The server action (or MCP tool, or CLI command) is glue code that resolves dependencies and calls the service. If an MCP tool and a server action do the same thing, they call the same service function.
- Inject dependencies, don't import them in services. Services receive their database client, logger, or any I/O capability as constructor arguments — never by importing framework-specific modules. This keeps them testable with stubs and reusable across interfaces.
- Schema in separate file - Reusable between client and server
- Logging - Always log before and after operations
- Revalidation - Use
revalidatePathafter mutations - Trust RLS - Don't add manual auth checks (RLS handles it)
- Testable in isolation - Because services accept their dependencies, you can test them with a mock client and no running infrastructure
File Structure
feature/
├── _lib/
│ ├── schemas/
│ │ └── feature.schema.ts
│ └── server/
│ ├── feature.service.ts
│ └── server-actions.ts
└── _components/
└── feature-form.tsx
Reference Files
See examples in:
[Examples](examples.md)[Reference](reference.md)