Server Actions Package (@schwab/server-actions)
Overview
The @schwab/server-actions package provides server-side action implementations for Next.js Server Actions, enabling secure server-side operations including lead generation processing, Vercel deployment management, and other server-only operations. This package ensures type-safe, validated, and secure handling of server-side business logic.
Architecture
Core Server Actions
1. Lead Generation Server Action
SFMC Lead Processing:
Lead Generation Server Action
// src/sfmc/LeadGenServerAction.ts
'use server';
import { postLeadData } from '@schwab/fetch/postLeadData';
import { validateLeadData } from '@schwab/utilities/validation/validateLeadData';
import { SchLogger } from '@schwab/utilities/SchLogger';
import { z } from 'zod';
import validator from 'validator';
const LeadFormSchema = z.object({
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
email: z.string().email(),
phone: z.string().refine(
(phone) => validator.isMobilePhone(phone, 'en-US'),
{ message: 'Invalid phone number format' }
),
investmentAmount: z.number().min(1000),
investmentGoals: z.array(z.string()).min(1),
marketingConsent: z.boolean(),
privacyPolicyAccepted: z.boolean().refine(val => val === true),
});
export async function LeadGenServerAction(
prevState: any,
formData: FormData
): Promise<{
success: boolean;
message: string;
errors?: Record<string, string[]>;
}> {
try {
// Extract and validate form data
const rawData = {
firstName: formData.get('firstName')?.toString(),
lastName: formData.get('lastName')?.toString(),
email: formData.get('email')?.toString(),
phone: formData.get('phone')?.toString(),
investmentAmount: Number(formData.get('investmentAmount')),
investmentGoals: formData.getAll('investmentGoals').map(String),
marketingConsent: formData.get('marketingConsent') === 'true',
privacyPolicyAccepted: formData.get('privacyPolicyAccepted') === 'true',
};
// Validate with Zod schema
const validationResult = LeadFormSchema.safeParse(rawData);
if (!validationResult.success) {
return {
success: false,
message: 'Validation failed',
errors: validationResult.error.flatten().fieldErrors,
};
}
const validatedData = validationResult.data;
// Additional business validation
const businessValidation = await validateLeadData(validatedData);
if (!businessValidation.isValid) {
return {
success: false,
message: businessValidation.message,
errors: businessValidation.errors,
};
}
// Submit to Salesforce Marketing Cloud
const sfmcResponse = await postLeadData({
...validatedData,
source: 'website_form',
timestamp: new Date().toISOString(),
userAgent: headers().get('user-agent') || '',
ipAddress: headers().get('x-forwarded-for') || '',
});
if (!sfmcResponse.isOk) {
SchLogger.error('SFMC lead submission failed', {
error: sfmcResponse.error,
email: validatedData.email,
});
return {
success: false,
message: 'Failed to submit lead. Please try again.',
};
}
// Log successful submission
SchLogger.info('Lead submitted successfully', {
leadId: sfmcResponse.data.leadId,
email: validatedData.email,
});
return {
success: true,
message: 'Thank you for your interest! We will contact you soon.',
};
} catch (error) {
SchLogger.error('Lead generation server action error', { error });
return {
success: false,
message: 'An unexpected error occurred. Please try again.',
};
}
}
2. Vercel Deployment Management
Aliaser Action for Deployment Management:
Vercel Aliaser Action
// src/vercel/AliaserAction.ts
'use server';
import { setAlias } from '@schwab/fetch/setAlias';
import { getDomainAliases } from '@schwab/fetch/getDomainAliases';
import { getDeployments } from '@schwab/fetch/getDeployments';
import { SchLogger } from '@schwab/utilities/SchLogger';
import { z } from 'zod';
const AliasActionSchema = z.object({
deploymentId: z.string().min(1),
alias: z.string().url(),
projectId: z.string().min(1),
teamId: z.string().optional(),
});
export async function AliaserAction(
prevState: any,
formData: FormData
): Promise<{
success: boolean;
message: string;
aliasUrl?: string;
errors?: Record<string, string[]>;
}> {
try {
// Validate user permissions (implement according to your auth system)
const isAuthorized = await checkDeploymentPermissions();
if (!isAuthorized) {
return {
success: false,
message: 'Unauthorized: Insufficient permissions for deployment management.',
};
}
// Extract and validate form data
const rawData = {
deploymentId: formData.get('deploymentId')?.toString(),
alias: formData.get('alias')?.toString(),
projectId: formData.get('projectId')?.toString(),
teamId: formData.get('teamId')?.toString(),
};
const validationResult = AliasActionSchema.safeParse(rawData);
if (!validationResult.success) {
return {
success: false,
message: 'Invalid input data',
errors: validationResult.error.flatten().fieldErrors,
};
}
const { deploymentId, alias, projectId, teamId } = validationResult.data;
// Verify deployment exists and is ready
const deployments = await getDeployments(projectId);
const targetDeployment = deployments.data?.find(d => d.id === deploymentId);
if (!targetDeployment) {
return {
success: false,
message: 'Deployment not found or not accessible.',
};
}
if (targetDeployment.state !== 'READY') {
return {
success: false,
message: 'Deployment is not ready for aliasing.',
};
}
// Check if alias is already in use
const existingAliases = await getDomainAliases(projectId);
const aliasExists = existingAliases.data?.some(a => a.name === alias);
if (aliasExists) {
return {
success: false,
message: 'Alias is already in use.',
};
}
// Set the alias
const aliasResponse = await setAlias({
deploymentId,
alias,
projectId,
teamId,
});
if (!aliasResponse.isOk) {
SchLogger.error('Vercel alias creation failed', {
error: aliasResponse.error,
deploymentId,
alias,
});
return {
success: false,
message: 'Failed to create alias. Please try again.',
};
}
SchLogger.info('Vercel alias created successfully', {
deploymentId,
alias,
projectId,
});
return {
success: true,
message: 'Alias created successfully!',
aliasUrl: alias,
};
} catch (error) {
SchLogger.error('Aliaser action error', { error });
return {
success: false,
message: 'An unexpected error occurred. Please try again.',
};
}
}
async function checkDeploymentPermissions(): Promise<boolean> {
// Implement your authentication/authorization logic here
// This could check user roles, API keys, session validity, etc.
return true; // Placeholder
}
Advanced Server Action Patterns
1. Multi-Step Form Processing
Multi-Step Server Action
// Complex multi-step form processing with state management
export async function MultiStepFormAction(
prevState: {
step: number;
data: Record<string, any>;
errors?: Record<string, string[]>;
},
formData: FormData
) {
const currentStep = Number(formData.get('currentStep')) || 1;
const action = formData.get('action')?.toString() || 'next';
try {
switch (currentStep) {
case 1:
return await processPersonalInfo(prevState, formData, action);
case 2:
return await processFinancialInfo(prevState, formData, action);
case 3:
return await processPreferences(prevState, formData, action);
case 4:
return await finalizeSubmission(prevState, formData);
default:
return {
step: 1,
data: {},
errors: { general: ['Invalid step'] },
};
}
} catch (error) {
SchLogger.error('Multi-step form error', { error, step: currentStep });
return {
...prevState,
errors: { general: ['An unexpected error occurred'] },
};
}
}
2. File Upload Server Action
Secure File Upload Action
export async function FileUploadAction(
prevState: any,
formData: FormData
): Promise<{
success: boolean;
message: string;
fileUrl?: string;
errors?: Record<string, string[]>;
}> {
try {
const file = formData.get('file') as File;
if (!file) {
return {
success: false,
message: 'No file provided',
};
}
// Validate file type and size
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
const maxSize = 5 * 1024 * 1024; // 5MB
if (!allowedTypes.includes(file.type)) {
return {
success: false,
message: 'Invalid file type. Only PDF, JPEG, and PNG files are allowed.',
};
}
if (file.size > maxSize) {
return {
success: false,
message: 'File size exceeds 5MB limit.',
};
}
// Secure file processing
const fileBuffer = Buffer.from(await file.arrayBuffer());
const fileName = `${Date.now()}-${file.name}`;
// Upload to secure storage (implement according to your storage solution)
const uploadResult = await uploadToSecureStorage(fileBuffer, fileName, file.type);
if (!uploadResult.success) {
return {
success: false,
message: 'File upload failed. Please try again.',
};
}
return {
success: true,
message: 'File uploaded successfully!',
fileUrl: uploadResult.url,
};
} catch (error) {
SchLogger.error('File upload action error', { error });
return {
success: false,
message: 'An unexpected error occurred during file upload.',
};
}
}
Security and Validation
1. Input Sanitization
Input Sanitization Utilities
import validator from 'validator';
export function sanitizeInput(input: string): string {
return validator.escape(validator.trim(input));
}
export function validateEmail(email: string): boolean {
return validator.isEmail(email) && !validator.contains(email, '+');
}
export function validatePhone(phone: string): boolean {
return validator.isMobilePhone(phone, 'en-US');
}
export function sanitizeHtml(html: string): string {
// Use a proper HTML sanitization library in production
return validator.escape(html);
}
2. Rate Limiting
Rate Limiting for Server Actions
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
export function rateLimit(
identifier: string,
maxRequests: number = 10,
windowMs: number = 60000
): boolean {
const now = Date.now();
const windowStart = now - windowMs;
const current = rateLimitMap.get(identifier);
if (!current || current.resetTime < windowStart) {
rateLimitMap.set(identifier, { count: 1, resetTime: now });
return true;
}
if (current.count >= maxRequests) {
return false;
}
current.count++;
return true;
}
// Usage in server actions
export async function RateLimitedServerAction(
prevState: any,
formData: FormData
) {
const clientIP = headers().get('x-forwarded-for') || 'unknown';
if (!rateLimit(clientIP, 5, 60000)) { // 5 requests per minute
return {
success: false,
message: 'Too many requests. Please try again later.',
};
}
// Continue with action logic...
}
3. CSRF Protection
CSRF Token Validation
export async function validateCSRFToken(formData: FormData): Promise<boolean> {
const token = formData.get('csrfToken')?.toString();
if (!token) {
return false;
}
// Validate token against session or stored value
const sessionToken = await getSessionCSRFToken();
return token === sessionToken;
}
export async function SecureServerAction(
prevState: any,
formData: FormData
) {
// Validate CSRF token
const isValidCSRF = await validateCSRFToken(formData);
if (!isValidCSRF) {
return {
success: false,
message: 'Invalid security token. Please refresh and try again.',
};
}
// Continue with secure action logic...
}
Testing Server Actions
1. Server Action Test Utilities
Server Action Testing
import { jest } from '@jest/globals';
export function createMockFormData(data: Record<string, string | string[]>): FormData {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => formData.append(key, v));
} else {
formData.append(key, value);
}
});
return formData;
}
export function mockServerActionDependencies() {
// Mock external dependencies
jest.mock('@schwab/fetch/postLeadData', () => ({
postLeadData: jest.fn().mockResolvedValue({
isOk: true,
data: { leadId: 'test-lead-123' }
})
}));
jest.mock('@schwab/utilities/SchLogger', () => ({
SchLogger: {
info: jest.fn(),
error: jest.fn(),
}
}));
}
// Example test
describe('LeadGenServerAction', () => {
beforeEach(() => {
mockServerActionDependencies();
});
it('should successfully process valid lead data', async () => {
const mockFormData = createMockFormData({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
investmentAmount: '10000',
investmentGoals: ['retirement'],
marketingConsent: 'true',
privacyPolicyAccepted: 'true',
});
const result = await LeadGenServerAction({}, mockFormData);
expect(result.success).toBe(true);
expect(result.message).toContain('Thank you for your interest');
});
it('should return validation errors for invalid data', async () => {
const mockFormData = createMockFormData({
firstName: '', // Invalid: empty
email: 'invalid-email', // Invalid: not an email
});
const result = await LeadGenServerAction({}, mockFormData);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors?.firstName).toBeDefined();
expect(result.errors?.email).toBeDefined();
});
});
Performance Optimization
1. Action Caching
Server Action Caching
import { cache } from 'react';
// Cache expensive server action operations
export const getCachedUserData = cache(async (userId: string) => {
// Expensive user data fetch
return await fetchUserDataFromAPI(userId);
});
// Use in server actions
export async function UserDependentAction(
prevState: any,
formData: FormData
) {
const userId = formData.get('userId')?.toString();
if (!userId) {
return { success: false, message: 'User ID required' };
}
// This will be cached across the request
const userData = await getCachedUserData(userId);
// Process action with cached user data
}
Error Handling and Monitoring
1. Structured Error Handling
Error Handling Pattern
export class ServerActionError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = 'ServerActionError';
}
}
export function handleServerActionError(error: unknown): {
success: false;
message: string;
code?: string;
} {
if (error instanceof ServerActionError) {
return {
success: false,
message: error.message,
code: error.code,
};
}
if (error instanceof z.ZodError) {
return {
success: false,
message: 'Validation failed',
code: 'VALIDATION_ERROR',
};
}
// Log unexpected errors
SchLogger.error('Unexpected server action error', { error });
return {
success: false,
message: 'An unexpected error occurred. Please try again.',
code: 'UNEXPECTED_ERROR',
};
}
Future Enhancements
Planned Improvements
- Enhanced Security: Advanced input validation and sanitization
- Performance Optimization: Action result caching and optimization
- Monitoring Integration: Comprehensive action performance monitoring
- Advanced Validation: Complex business rule validation
- Workflow Management: Multi-step workflow orchestration
The Server Actions package provides secure, validated, and performant server-side operations that enable robust form processing, data submission, and server-side business logic execution across the Charles Schwab monorepo.