- Use ESM for building the MCP Server - Added own Postgres dependency to MCP Server for querying tables and other entities in MCP - Vastly improved AI Agent rules - Added MCP Prompts for reviewing code and planning features - Minor refactoring
11 KiB
11 KiB
Next.js Utilities Instructions
This file contains instructions for working with Next.js utilities including server actions and route handlers.
Server Actions Implementation
Always use enhanceAction from @packages/next/src/actions/index.ts:
'use server';
import { enhanceAction } from '@kit/next/actions';
import { z } from 'zod';
// Define your schema
const CreateNoteSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
accountId: z.string().uuid('Invalid account ID'),
});
export const createNoteAction = enhanceAction(
async function (data, user) {
// data is automatically validated against the schema
// user is automatically authenticated if auth: true
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
account_id: data.accountId,
user_id: user.id,
})
.select()
.single();
if (error) {
throw error;
}
return { success: true, note };
},
{
auth: true, // Require authentication
schema: CreateNoteSchema, // Validate input with Zod
},
);
Server Action Examples
- Team billing:
@apps/web/app/home/[account]/billing/_lib/server/server-actions.ts - Personal settings:
@apps/web/app/home/(user)/settings/_lib/server/server-actions.ts
Server Action Options
export const myAction = enhanceAction(
async function (data, user) {
// data: validated input data
// user: authenticated user (if auth: true)
return { success: true };
},
{
auth: true, // Require authentication (default: false)
schema: MySchema, // Zod schema for validation (optional)
// Additional options available
},
);
Route Handlers (API Routes)
Use enhanceRouteHandler from @packages/next/src/routes/index.ts:
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
import { z } from 'zod';
// Define your schema
const CreateItemSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
});
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated against schema
// user is available if auth: true
// request is the original NextRequest
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.insert({
name: body.name,
description: body.description,
user_id: user.id,
})
.select()
.single();
if (error) {
return NextResponse.json(
{ error: 'Failed to create item' },
{ status: 500 }
);
}
return NextResponse.json({ success: true, data });
},
{
auth: true, // Require authentication
schema: CreateItemSchema, // Validate request body
},
);
export const GET = enhanceRouteHandler(
async function ({ user, request }) {
const url = new URL(request.url);
const limit = url.searchParams.get('limit') || '10';
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.select('*')
.eq('user_id', user.id)
.limit(parseInt(limit));
if (error) {
return NextResponse.json(
{ error: 'Failed to fetch items' },
{ status: 500 }
);
}
return NextResponse.json({ data });
},
{
auth: true,
// No schema needed for GET requests
},
);
Route Handler Options
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// Handler function
return NextResponse.json({ success: true });
},
{
auth: true, // Require authentication (default: false)
schema: MySchema, // Zod schema for body validation (optional)
// Additional options available
},
);
Revalidation
- Use
revalidatePathfor revalidating data after a migration. - Avoid calling
router.refresh()orrouter.push()following a Server Action. UserevalidatePathandredirectfrom the server action instead.
Error Handling Patterns
Server Actions with Error Handling
export const createNoteAction = enhanceAction(
async function (data, user) {
const logger = await getLogger();
const ctx = { name: 'create-note', userId: user.id };
try {
logger.info(ctx, 'Creating note');
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
user_id: user.id,
})
.select()
.single();
if (error) {
logger.error({ ...ctx, error }, 'Failed to create note');
throw error;
}
logger.info({ ...ctx, noteId: note.id }, 'Note created successfully');
return { success: true, note };
} catch (error) {
if (!isRedirectError(error)) {
logger.error({ ...ctx, error }, 'Create note action failed');
throw error;
}
}
},
{
auth: true,
schema: CreateNoteSchema,
},
);
Server Action Redirects - Client Handling
When server actions call redirect(), it throws a special error that should NOT be treated as a failure:
import { isRedirectError } from 'next/dist/client/components/redirect-error';
async function handleSubmit(formData: FormData) {
try {
await myServerAction(formData);
} catch (error) {
// Don't treat redirects as errors
if (!isRedirectError(error)) {
// Handle actual errors
toast.error('Something went wrong');
}
}
}
### Route Handler with Error Handling
```typescript
export const POST = enhanceRouteHandler(
async function ({ body, user }) {
const logger = await getLogger();
const ctx = { name: 'api-create-item', userId: user.id };
try {
logger.info(ctx, 'Processing API request');
// Process request
const result = await processRequest(body, user);
logger.info({ ...ctx, result }, 'API request successful');
return NextResponse.json({ success: true, data: result });
} catch (error) {
logger.error({ ...ctx, error }, 'API request failed');
if (error.message.includes('validation')) {
return NextResponse.json(
{ error: 'Invalid input data' },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
{
auth: true,
schema: CreateItemSchema,
},
);
Client-Side Integration
Using Server Actions in Components
'use client';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from '@kit/ui/sonner';
import { Button } from '@kit/ui/button';
import { createNoteAction } from './actions';
import { CreateNoteSchema } from './schemas';
function CreateNoteForm() {
const [isPending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
try {
const result = await createNoteAction(data);
if (result.success) {
toast.success('Note created successfully!');
form.reset();
}
} catch (error) {
toast.error('Failed to create note');
console.error('Create note error:', error);
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
<Button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Note'}
</Button>
</form>
);
}
NB: When using redirect, we must handle it using isRedirectError otherwise we display an error after the server action succeeds
Using Route Handlers with Fetch
'use client';
async function createItem(data: CreateItemInput) {
const response = await fetch('/api/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create item');
}
return response.json();
}
// Usage in component
const handleCreateItem = async (data) => {
try {
const result = await createItem(data);
toast.success('Item created successfully!');
return result;
} catch (error) {
toast.error('Failed to create item');
throw error;
}
};
Security Best Practices
Input Validation
Always use Zod schemas for input validation:
// Define strict schemas
const UpdateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120),
});
// Server action with validation
export const updateUserAction = enhanceAction(
async function (data, user) {
// data is guaranteed to match the schema
// Additional business logic validation can go here
if (data.email !== user.email) {
// Check if email change is allowed
const canChangeEmail = await checkEmailChangePermission(user);
if (!canChangeEmail) {
throw new Error('Email change not allowed');
}
}
// Update user
return await updateUser(user.id, data);
},
{
auth: true,
schema: UpdateUserSchema,
},
);
Authorization Checks
export const deleteAccountAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
// Verify user owns the account
const { data: account, error } = await client
.from('accounts')
.select('id, primary_owner_user_id')
.eq('id', data.accountId)
.single();
if (error || !account) {
throw new Error('Account not found');
}
if (account.primary_owner_user_id !== user.id) {
throw new Error('Only account owners can delete accounts');
}
// Additional checks
const hasActiveSubscription = await client
.rpc('has_active_subscription', { account_id: data.accountId });
if (hasActiveSubscription) {
throw new Error('Cannot delete account with active subscription');
}
// Proceed with deletion
await deleteAccount(data.accountId);
return { success: true };
},
{
auth: true,
schema: DeleteAccountSchema,
},
);