NJ Municipality Lookup
CodebaseSrcApp(app)

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:

  1. Accept form submissions and user input
  2. Validate and sanitize input data
  3. Execute domain use cases with dependency injection
  4. Handle errors with user-friendly messages
  5. 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 ErrorUser Message
AddressNotFoundError"Address not found. Please check your input..."
ValidationErrorSpecific 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:

  1. Input Validation: XSS prevention, injection protection, length limits
  2. Output Encoding: Sanitized error messages
  3. Rate Limiting: Enforced at adapter level (configurable per endpoint)
  4. Error Handling: No sensitive information in error responses
  5. 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:

  1. Unit Tests: Mock dependencies, verify error handling
  2. Integration Tests: Test with real domain use cases
  3. E2E Tests: Playwright tests submit forms and verify results

On this page