backend-api
This skill should be used when creating or modifying backend API endpoints, service layer classes, or integrating external APIs (Reddit, OpenAI) in the ClaudeCode Sentiment Monitor project. Specifically trigger this skill for tasks involving Next.js App Router API routes, singleton services, authentication, validation, or business logic implementation.
SKILL.md
| Name | backend-api |
| Description | This skill should be used when creating or modifying backend API endpoints, service layer classes, or integrating external APIs (Reddit, OpenAI) in the ClaudeCode Sentiment Monitor project. Specifically trigger this skill for tasks involving Next.js App Router API routes, singleton services, authentication, validation, or business logic implementation. |
name: backend-api description: This skill should be used when creating or modifying backend API endpoints, service layer classes, or integrating external APIs (Reddit, OpenAI) in the ClaudeCode Sentiment Monitor project. Specifically trigger this skill for tasks involving Next.js App Router API routes, singleton services, authentication, validation, or business logic implementation.
Backend API Development
Overview
Guide for developing backend API endpoints and service layer classes in the ClaudeCode Sentiment Monitor project using Next.js 15 App Router, TypeScript, and Prisma ORM.
When to Use This Skill
Use this skill when:
- Creating new API endpoints
- Modifying existing API routes
- Creating or updating service layer classes
- Integrating with Reddit or OpenAI APIs
- Adding authentication or validation
- Implementing business logic
- Debugging backend errors
Architecture
Three-Layer Pattern
API Routes (app/api/) ← Thin HTTP adapters
↓
Service Layer (lib/services/) ← Business logic (singletons)
↓
Database (Prisma ORM) ← Data persistence
Key principle: API routes are thin adapters. All business logic lives in service classes.
Quick Start
Create a New Endpoint
Use the bundled script:
# From project root
.claude/skills/backend-api/scripts/create_endpoint.py dashboard/stats GET
.claude/skills/backend-api/scripts/create_endpoint.py ingest/refresh POST --protected
This generates:
- Route file at
app/api/{endpoint-path}/route.ts - Zod validation schema
- Error handling boilerplate
- Authentication check (if
--protected) - Cache headers (for GET)
Then customize:
- Update validation schema
- Import and call appropriate service
- Test locally
Manual Endpoint Creation
If not using the script, follow this structure:
// app/api/endpoint/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { exampleService } from "@/lib/services/example.service";
const QuerySchema = z.object({
param: z.string(),
});
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const params = QuerySchema.parse({
param: searchParams.get("param"),
});
const result = await exampleService.doSomething(params.param);
return NextResponse.json(result, {
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
});
} catch (error) {
console.error("Error in GET /api/endpoint:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid parameters", details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Service Layer
Creating a Service
All services must follow the singleton pattern. See references/service-patterns.md for complete examples and existing services (RedditService, SentimentService, AggregationService).
Template:
// lib/services/example.service.ts
import { PrismaClient } from "@/generated/prisma/client";
export class ExampleService {
private static instance: ExampleService;
private prisma: PrismaClient;
private constructor() {
this.prisma = new PrismaClient();
}
public static getInstance(): ExampleService {
if (!ExampleService.instance) {
ExampleService.instance = new ExampleService();
}
return ExampleService.instance;
}
public async doSomething(param: string): Promise<Result> {
// Business logic here
}
}
export const exampleService = ExampleService.getInstance();
Existing Services
Review references/service-patterns.md for details on:
- RedditService - Reddit API with OAuth, rate limiting, caching
- SentimentService - OpenAI analysis with 7-day cache
- AggregationService - Daily stats and drill-down queries
Authentication
Protected endpoints (e.g., /api/ingest/*) require CRON_SECRET:
function verifyAuth(request: NextRequest): boolean {
const authHeader = request.headers.get("authorization");
const token = authHeader?.replace("Bearer ", "");
return token === process.env.CRON_SECRET;
}
export async function POST(request: NextRequest) {
if (!verifyAuth(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Protected logic
}
Public endpoints: /api/dashboard/*, /api/drill-down, /api/export/*
Validation with Zod
Always validate inputs:
import { z } from "zod";
// Define schema
const RequestSchema = z.object({
range: z.enum(["7d", "30d", "90d"]).default("7d"),
subreddit: z.enum(["all", "ClaudeAI", "ClaudeCode", "Anthropic"]).default("all"),
});
// Validate query params
const { searchParams } = new URL(request.url);
const params = RequestSchema.parse({
range: searchParams.get("range"),
subreddit: searchParams.get("subreddit"),
});
// Validate request body (POST)
const body = await request.json();
const params = RequestSchema.parse(body);
Error Handling
Comprehensive error handling pattern:
try {
// Logic
} catch (error) {
console.error("Error context:", error);
// Zod validation errors (400)
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request", details: error.errors },
{ status: 400 }
);
}
// Prisma errors
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
return NextResponse.json(
{ error: "Resource already exists" },
{ status: 409 }
);
}
}
// Generic error (500)
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
Cache Headers
GET endpoints should use CDN-friendly caching:
// Dashboard data (30 minutes)
return NextResponse.json(data, {
headers: {
"Cache-Control": "public, s-maxage=1800, stale-while-revalidate=3600",
},
});
// Drill-down data (5 minutes)
return NextResponse.json(data, {
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
});
Common Workflows
Polling Endpoint Pattern
export async function POST(request: NextRequest) {
if (!verifyAuth(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const results = { /* ... */ };
for (const subreddit of ["ClaudeAI", "ClaudeCode", "Anthropic"]) {
try {
// 1. Fetch new posts
const posts = await redditService.fetchPosts(subreddit, { limit: 25 });
// 2. Save to database
for (const post of posts) {
await prisma.rawPost.upsert({ /* ... */ });
results[subreddit].newPosts++;
}
// 3. Analyze sentiment
await sentimentService.analyzeBatch(items, { batchSize: 20 });
// 4. Recompute aggregates
await aggregationService.recomputeRange(yesterday, today, [subreddit]);
} catch (error) {
console.error(`Error polling ${subreddit}:`, error);
// Continue to next subreddit
}
}
return NextResponse.json({ success: true, results });
}
Testing Locally
cd app
npm run dev # Start server on http://localhost:3000
# Test GET endpoint
curl "http://localhost:3000/api/dashboard/data?range=7d&subreddit=all"
# Test protected POST
curl -X POST http://localhost:3000/api/ingest/poll \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json"
# Test validation (should return 400)
curl "http://localhost:3000/api/dashboard/data?range=invalid"
Environment Variables
Required in app/.env.local:
DATABASE_URL="postgresql://..."
REDDIT_CLIENT_ID="..."
REDDIT_CLIENT_SECRET="..."
REDDIT_USER_AGENT="ClaudeCodeMonitor/1.0"
OPENAI_API_KEY="sk-..."
CRON_SECRET="secure-random-string"
Access in code: process.env.VARIABLE_NAME
Common Pitfalls
Avoid these mistakes:
- Business logic in routes - Use service layer instead
- Multiple service instances - Always use singleton pattern
- No input validation - Always use Zod schemas
- Exposing CRON_SECRET - Never log or return in responses
- Missing cache headers - GET endpoints should be cached
- Not logging errors - Always log with context
- Hardcoded values - Use environment variables
- Skipping error handling - Handle Zod, Prisma, external API errors
Checklist
When creating/modifying an endpoint:
- Route file in correct directory (
app/api/endpoint/route.ts) - Exported function matches HTTP method (
GET,POST, etc.) - Input validation uses Zod schemas
- Protected endpoints verify CRON_SECRET
- Business logic in service layer (not route)
- Errors caught and logged with context
- Appropriate HTTP status codes
- GET endpoints have Cache-Control headers
- Response format is JSON
- Tested locally before committing
Resources
- Next.js API Routes: https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- Zod Docs: https://zod.dev/
- Service Patterns: See
references/service-patterns.mdin this skill - Prisma Docs: https://www.prisma.io/docs
Examples
See existing implementations:
app/api/dashboard/data/route.ts- GET with cachingapp/api/ingest/poll/route.ts- Protected POSTapp/lib/services/- Service layer patterns