Actions
Next.js Server Actions providing type-safe, progressive enhancement server functions for address geocoding and bulk processing.
Server Actions (app/actions/)
Next.js Server Actions providing type-safe, progressive enhancement server functions for address geocoding and bulk processing.
Purpose
Bridge the gap between React UI components and domain layer business logic:
- Accept form submissions and user input
- Validate and sanitize input data
- Execute domain use cases with dependency injection
- Handle errors with user-friendly messages
- Return serialized results to client components
Server Actions Architecture
Server Actions are async functions marked with "use server" that:
- Run exclusively on the server (never in the browser)
- Can be called directly from client components
- Provide automatic request/response serialization
- Enable progressive enhancement (work without JavaScript)
- Offer type-safe end-to-end communication
Actions
geocoding.ts - Single Address Operations
Primary server actions for individual address lookups:
import { geocodeAddress, getAddressSuggestions } from "@/app/actions/geocoding";
// Geocode full address
const result = await geocodeAddress("33 Washington St, Newark, NJ");
if (result.success) {
console.log(result.data.municipality.name); // "Newark"
}
// Get autocomplete suggestions
const suggestions = await getAddressSuggestions("123 Main", 5);
if (suggestions.success) {
suggestions.data.forEach((s) => console.log(s.text));
}
// Get cache performance statistics
const stats = await getCacheStats();
Responsibilities:
- Input validation with XSS/injection prevention
- Domain entity creation from raw strings
- Use case execution with error handling
- Error mapping to user-friendly messages
- Cache hit/miss tracking
bulk-geocoding.ts - Bulk Address Operations
Server action for processing multiple addresses:
import { geocodeAddressesBulk } from "@/app/actions/bulk-geocoding";
const addresses = ["Address 1", "Address 2", "Address 3"];
const results = await geocodeAddressesBulk(addresses);
if (results.success) {
results.data.forEach((result) => {
console.log(result.municipality?.name ?? "Not found");
});
}
Features:
- Up to 1,000 addresses per request
- Parallel processing with rate limiting
- Partial success handling (some addresses may fail)
- Progress tracking for UI updates
geocoding-structured.ts - Structured Address Lookup
Server action for pre-parsed address components:
import { geocodeStructuredAddress } from "@/app/actions/geocoding-structured";
const result = await geocodeStructuredAddress({
streetAddress: "323 Dr Martin Luther King Jr Blvd",
city: "Newark",
zipCode: "07102",
});
Use Cases:
- Form submissions with separate address fields
- Integration with structured address data sources
- Pre-validated address components
api-testing.ts - Development Testing Actions
Server actions for testing NJ APIs during development:
import {
testGeocodingApi,
testSuggestionsApi,
} from "@/app/actions/api-testing";
// Test geocoding API with raw parameters
const geoResult = await testGeocodingApi({
address: "Test Address",
format: "json",
});
// Test suggestions API
const sugResult = await testSuggestionsApi({
query: "123",
maxResults: 10,
});
Purpose:
- Debug API integration issues
- Inspect raw API responses
- Test edge cases and error conditions
Response Format
All server actions return a discriminated union type:
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
This pattern enables type-safe error handling:
const result = await geocodeAddress(address);
if (!result.success) {
// TypeScript knows result.error is a string
showError(result.error);
return;
}
// TypeScript knows result.data is GeocodingResult
console.log(result.data.municipality.name);
Error Handling
Server actions map domain errors to user-friendly messages:
| Domain Error | User Message |
|---|---|
AddressNotFoundError | "Address not found. Please check your input..." |
ValidationError | Specific validation failure message |
ApiTimeoutError | "Request timed out. Please check your connection..." |
RateLimitError | "Too many requests. Please wait..." |
| Unknown errors | "An unexpected error occurred. Please try again." |
Stack traces and sensitive details are logged server-side only, never exposed to clients.
Security
All server actions implement OWASP Top 10 protections:
- Input Validation: XSS prevention, injection protection, length limits
- Output Encoding: Sanitized error messages
- Rate Limiting: Enforced at adapter level (configurable per endpoint)
- Error Handling: No sensitive information in error responses
- CSRF Protection: Built-in with Next.js Server Actions
Performance
Server actions are optimized for low latency:
- Cache Hit: < 50ms response time
- Cache Miss: 200-500ms (depends on NJ OGIS API)
- Bulk Processing: Parallel execution with rate limiting
- Zero Client Bundle: Server actions don't increase client JavaScript size
Usage Patterns
Form Actions (Progressive Enhancement)
export function AddressForm() {
return (
<form
action={async (formData) => {
"use server";
const address = formData.get("address") as string;
const result = await geocodeAddress(address);
redirect(`/results?data=${encodeURIComponent(JSON.stringify(result))}`);
}}
>
<input name="address" required />
<button type="submit">Lookup</button>
</form>
);
}
Client Component with useTransition
"use client";
import { useTransition } from "react";
import { geocodeAddress } from "@/app/actions/geocoding";
export function AddressLookup() {
const [result, setResult] = useState(null);
const [isPending, startTransition] = useTransition();
function handleSubmit(formData: FormData) {
startTransition(async () => {
const address = formData.get("address") as string;
const geocodingResult = await geocodeAddress(address);
if (geocodingResult.success) {
setResult(geocodingResult.data);
} else {
alert(geocodingResult.error);
}
});
}
return (
<form action={handleSubmit}>
<input name="address" />
<button disabled={isPending}>
{isPending ? "Looking up..." : "Lookup"}
</button>
{result && <div>Municipality: {result.municipality.name}</div>}
</form>
);
}
Testing
Server actions are tested at multiple levels:
- Unit Tests: Mock dependencies, verify error handling
- Integration Tests: Test with real domain use cases
- E2E Tests: Playwright tests submit forms and verify results
Related Documentation
- Domain Use Cases - Business logic executed by actions
- Validation - Input validation utilities
- Error Types - Domain error classes
- NJ API Adapters - External API integration