* MCP Server 2.0 - Updated application version from 2.23.14 to 2.24.0 in package.json. - MCP Server improved with new features - Migrated functionality from Dev Tools to MCP Server - Improved getMonitoringProvider not to crash application when misconfigured
137 lines
4.5 KiB
Markdown
137 lines
4.5 KiB
Markdown
---
|
|
name: server-action-builder
|
|
description: 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/`:
|
|
|
|
```typescript
|
|
// _lib/schemas/feature.schema.ts
|
|
import { 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.infer<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/`:
|
|
|
|
```typescript
|
|
// _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`:
|
|
|
|
```typescript
|
|
'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
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **Schema in separate file** - Reusable between client and server
|
|
4. **Logging** - Always log before and after operations
|
|
5. **Revalidation** - Use `revalidatePath` after mutations
|
|
6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
|
7. **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)`
|