smithery/planetaryescape

auto-generated-nextjs-api-route-patterns

Next.js API route patterns for this project. Runtime config, circuit breakers, demo mode fallback, multiple AWS invocation strategies. Triggers on "api route", "next.js", "route handler", "POST", "GET".

Installation

$ npx skills add smithery/planetaryescape --skill auto-generated-nextjs-api-route-patterns

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/planetaryescape.

npx skills add smithery/planetaryescape

Browse all from smithery/planetaryescape

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,034 B
  • docs SUMMARY.md 250 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Next.js API Route Patterns

API routes in frontend/app/api/ with resilience, demo mode, and multiple backend invocation methods.

Route Handler Exports

Always export runtime and optionally maxDuration:

// From frontend/app/api/digest/trigger/route.ts
export const runtime = "nodejs";
export const maxDuration = 60; // seconds, for long-running operations

Export named functions for HTTP methods:

export async function POST(request: Request) {
  // handler logic
}

export async function GET(request: Request) {
  // handler logic
}

Clerk Auth Pattern (Currently Disabled)

Auth commented out until Clerk configured. Use demo user as fallback:

// From frontend/app/api/digest/trigger/route.ts
// Auth temporarily disabled while Clerk is not configured
// const { userId } = await auth();
// if (!userId) {
//   return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// }
const userId = "demo-user";

Rate Limiting

Check rate limits before expensive operations:

// From frontend/app/api/digest/trigger/route.ts
const rateLimitResult = checkRateLimit(userId, "digest-trigger");
if (!rateLimitResult.allowed) {
  return NextResponse.json(
    { error: "Too many requests. Please try again later." },
    {
      status: 429,
      headers: {
        "Retry-After": rateLimitResult.retryAfter?.toString() || "3600",
      },
    }
  );
}

Rate limiter tracks by ${userId}:${endpoint} key. Default: 10 requests/hour.

Demo Mode Fallback

Return demo data when AWS credentials not configured:

// From frontend/app/api/digest/trigger/route.ts
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
  return NextResponse.json({
    success: true,
    message: "Demo mode: Digest generation simulated (AWS credentials not configured)",
    data: {
      demo: true,
      payload,
      generatedAt: new Date().toISOString(),
      estimatedCompletion: new Date(Date.now() + 120000).toISOString(),
    },
    type: "demo",
  });
}

For list endpoints, return realistic demo data:

// From frontend/app/api/stepfunctions/executions/route.ts
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
  const demoExecutions = [
    {
      executionArn: "arn:aws:states:us-east-1:123456789012:execution:ai-digest-pipeline:demo-execution-1",
      name: "demo-execution-1",
      status: "SUCCEEDED",
      startDate: new Date(Date.now() - 3600000).toISOString(),
      stopDate: new Date(Date.now() - 3000000).toISOString(),
    },
  ];

  return NextResponse.json({
    executions: demoExecutions,
    nextToken: null,
    demo: true,
  });
}

Multiple Invocation Strategies

Three ways to invoke backend, tried in order:

1. Step Functions (Orchestrated Pipeline)

// From frontend/app/api/digest/trigger/route.ts
if (useStepFunctions) {
  const stateMachineArn =
    process.env.STEP_FUNCTIONS_STATE_MACHINE_ARN ||
    process.env.NEXT_PUBLIC_STEP_FUNCTIONS_STATE_MACHINE_ARN;

  if (!stateMachineArn) {
    return NextResponse.json(
      {
        success: false,
        error: "Step Functions state machine ARN not configured",
        message: "Please configure STEP_FUNCTIONS_STATE_MACHINE_ARN in your environment variables",
        type: "configuration-error",
      },
      { status: 500 }
    );
  }

  const executionName = `digest-${Date.now()}-${userId.slice(-6)}`;

  const command = new StartExecutionCommand({
    stateMachineArn: stateMachineArn,
    name: executionName,
    input: JSON.stringify(payload),
  });

  const sfnClient = getSFNClient();

  const response = await sfnCircuitBreaker.execute(async () => {
    return await sfnClient.send(command);
  });

  return NextResponse.json({
    success: true,
    message: "Step Functions pipeline started",
    executionArn: response.executionArn,
    executionName,
    startDate: response.startDate,
    type: "stepfunctions",
  });
}

2. Lambda Function URL (Preferred)

// From frontend/app/api/digest/trigger/route.ts
const functionUrl = process.env.LAMBDA_RUN_NOW_URL;

if (functionUrl) {
  const response = await httpCircuitBreaker.execute(async () => {
    return await fetch(functionUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });
  });

  const data = await response.json();

  return NextResponse.json({
    success: response.ok,
    message: data.message || "Digest generation triggered via Function URL",
    data,
    type: "lambda-url",
  });
}

3. AWS SDK Invocation (Fallback)

// From frontend/app/api/digest/trigger/route.ts
const command = new InvokeCommand({
  FunctionName: process.env.LAMBDA_DIGEST_FUNCTION_NAME,
  InvocationType: cleanup ? "Event" : "RequestResponse", // async vs sync
  Payload: JSON.stringify(payload),
});

const lambda = getLambdaClient();

const response = await lambdaCircuitBreaker.execute(async () => {
  return await lambda.send(command);
});

if (response.StatusCode === 202) {
  return NextResponse.json({
    success: true,
    message: "Digest generation started (async mode)",
    requestId: response.$metadata.requestId,
    type: "lambda-sdk",
  });
}

const responsePayload = response.Payload
  ? JSON.parse(new TextDecoder().decode(response.Payload))
  : null;

return NextResponse.json({
  success: true,
  message: "Digest generation completed",
  data: responsePayload,
  type: "lambda-sdk",
});

Circuit Breaker Integration

Create circuit breakers at module level, reuse across requests:

// From frontend/app/api/digest/trigger/route.ts
const sfnCircuitBreaker = CircuitBreaker.getBreaker("stepfunctions-digest", {
  failureThreshold: 5,
  resetTimeout: 60000,
});

const lambdaCircuitBreaker = CircuitBreaker.getBreaker("lambda-digest", {
  failureThreshold: 5,
  resetTimeout: 60000,
});

const httpCircuitBreaker = CircuitBreaker.getBreaker("lambda-http", {
  failureThreshold: 5,
  resetTimeout: 60000,
});

Use singleton pattern via getBreaker() - creates on first call, returns existing on subsequent.

Wrap AWS SDK calls:

const response = await sfnCircuitBreaker.execute(async () => {
  return await sfnClient.send(command);
});

Input Validation with Zod

Validate query parameters:

// From frontend/app/api/stepfunctions/executions/route.ts
import { z } from "zod";

const querySchema = z.object({
  status: z.enum(["RUNNING", "SUCCEEDED", "FAILED", "TIMED_OUT", "ABORTED"]).optional(),
  maxResults: z
    .string()
    .optional()
    .default("10")
    .transform((val) => {
      const num = Number.parseInt(val, 10);
      if (Number.isNaN(num)) {
        return 10;
      }
      return Math.max(1, Math.min(num, 100));
    }),
  nextToken: z.string().optional(),
});

const { searchParams } = new URL(request.url);

const validationResult = querySchema.safeParse({
  status: searchParams.get("status") || undefined,
  maxResults: searchParams.get("maxResults") || "10",
  nextToken: searchParams.get("nextToken") || undefined,
});

if (!validationResult.success) {
  return NextResponse.json(
    { error: "Invalid query parameters", details: validationResult.error.issues },
    { status: 400 }
  );
}

const { status: statusFilter, maxResults, nextToken } = validationResult.data;

Error Sanitization

Always sanitize errors in responses:

// From frontend/app/api/digest/trigger/route.ts
import { sanitizeError } from "@/lib/utils/error-handling";

try {
  // route logic
} catch (error) {
  return NextResponse.json(
    {
      error: "Failed to trigger digest generation",
      details: sanitizeError(error), // hides sensitive info in production
    },
    { status: 500 }
  );
}

Sanitizer returns generic messages in production, full details in development.

Response Type Tagging

Tag responses with invocation method:

return NextResponse.json({
  success: true,
  message: "...",
  type: "stepfunctions", // or "lambda-url" or "lambda-sdk" or "demo"
});

Helps frontend distinguish between real and demo data.

AWS Client Initialization

Singleton clients with conditional credentials:

// From frontend/lib/aws/clients.ts
let sfnClient: SFNClient | null = null;

export function getSFNClient(): SFNClient {
  if (!sfnClient) {
    sfnClient = new SFNClient({
      region: process.env.AWS_REGION || "us-east-1",
      ...(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
        ? {
            credentials: {
              accessKeyId: process.env.AWS_ACCESS_KEY_ID,
              secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
            },
          }
        : {}),
    });
  }
  return sfnClient;
}

Pattern: only set credentials if both key and secret present, otherwise let SDK use default provider chain.

Key Files

  • frontend/app/api/digest/trigger/route.ts - POST endpoint with 3 invocation strategies
  • frontend/app/api/stepfunctions/executions/route.ts - GET endpoint with validation
  • frontend/lib/aws/clients.ts - Singleton AWS clients
  • frontend/lib/circuit-breaker.ts - Opossum wrapper
  • frontend/lib/rate-limiter.ts - In-memory rate limiting
  • frontend/lib/utils/error-handling.ts - Error sanitization

Avoid

  • Don't create new circuit breakers per request (use module-level singletons)
  • Don't skip demo mode checks (frontend must work without AWS)
  • Don't expose raw AWS errors (always use sanitizeError)
  • Don't forget rate limiting on expensive operations
  • Don't use relative timeout values (use absolute ms)
  • Don't skip input validation on GET endpoints