Install
$ agentstack add skill-cwinvestments-memstack-api-designer Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Reads credentials/environment and may exfiltrate them.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
🔌 API Designer — Designing routes, validation, and handler patterns...
Produces production-ready Next.js App Router API routes with auth guards, Zod validation, typed responses, and consistent error handling.
Activation
When this skill activates, output:
🔌 API Designer — Designing API routes and handlers...
Then execute the protocol below.
| Context | Status | |---------|--------| | User says "design API" or "create API route" or "add endpoint" | ACTIVE | | User says "REST API" or "route handler" | ACTIVE | | Building a new feature that needs API routes | ACTIVE | | Designing webhook endpoints | ACTIVE | | Discussing API concepts or REST theory | DORMANT | | Working on frontend components (not API layer) | DORMANT |
Anti-patterns
| Trap | Reality Check | |------|---------------| | "Auth is handled by middleware" | Middleware can be bypassed. Every route handler must verify auth independently. | | "I'll validate input later" | Unvalidated input is the root of injection, type errors, and 500s. Zod first, logic second. | | "Return 200 for everything" | Status codes are the API contract. 401 vs 403 vs 404 vs 422 mean different things to clients. | | "Error details help debugging" | Stack traces and internal errors help attackers. Return safe messages, log details server-side. | | "Rate limiting is a nice-to-have" | Public endpoints without rate limits get abused within hours of deployment. | | "TypeScript types are documentation enough" | Types exist at build time. Zod schemas validate at runtime. You need both. |
Protocol
Step 1: Design Route Structure
Map features to Next.js App Router file paths:
app/
api/
auth/
login/route.ts POST - authenticate user
logout/route.ts POST - clear session
callback/route.ts GET - OAuth callback
organizations/
route.ts GET - list user's orgs
POST - create org
[orgId]/
route.ts GET - get org details
PATCH - update org
DELETE - delete org
members/
route.ts GET - list members
POST - invite member
[memberId]/
route.ts PATCH - update role
DELETE - remove member
projects/
route.ts GET - list projects
POST - create project
webhooks/
stripe/route.ts POST - Stripe webhook
github/route.ts POST - GitHub webhook
Naming conventions:
| Convention | Rule | Example | |------------|------|---------| | Resource names | Plural nouns | /organizations, /projects | | Dynamic segments | [paramName] camelCase | [orgId], [memberId] | | Nested resources | Parent/child path | /organizations/[orgId]/projects | | Actions (non-CRUD) | Verb sub-path | /auth/login, /reports/generate | | Webhooks | /webhooks/{provider} | /webhooks/stripe |
Output route table:
Method Path Auth Description
GET /api/organizations ✅ List user's organizations
POST /api/organizations ✅ Create organization
GET /api/organizations/[orgId] ✅+org Get organization details
PATCH /api/organizations/[orgId] ✅+org Update organization
...
POST /api/webhooks/stripe 🔑sig Handle Stripe webhook
Step 2: Auth Guard Pattern
Every protected route starts with the same two-step auth chain:
import { getAuthContext } from '@/lib/auth';
import { verifyOrgAccess } from '@/lib/auth';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
req: NextRequest,
{ params }: { params: { orgId: string } }
) {
// Step 1: Authenticate — who is this user?
const auth = await getAuthContext(req);
if (!auth) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
// Step 2: Authorize — can they access this org?
const access = await verifyOrgAccess(auth.userId, params.orgId);
if (!access) {
return NextResponse.json(
{ error: 'Access denied' },
{ status: 403 }
);
}
// Now proceed with business logic...
}
Auth decision matrix:
| Route Type | Auth Required | Org Check | Example | |-----------|--------------|-----------|---------| | Public | ❌ | ❌ | GET /api/health | | Authenticated | ✅ | ❌ | GET /api/user/profile | | Org-scoped | ✅ | ✅ | GET /api/organizations/[orgId]/projects | | Admin-only | ✅ | ✅ + role check | DELETE /api/organizations/[orgId] | | Webhook | 🔑 Signature | ❌ | POST /api/webhooks/stripe |
Step 3: Input Validation with Zod
Every route that accepts input validates it before any logic runs:
import { z } from 'zod';
// Define schema next to the route handler
const createProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
status: z.enum(['draft', 'active', 'archived']).default('draft'),
settings: z.object({
isPublic: z.boolean().default(false),
tags: z.array(z.string().max(50)).max(10).default([]),
}).optional(),
});
type CreateProjectInput = z.infer;
export async function POST(req: NextRequest) {
// ... auth checks ...
// Validate input
const body = await req.json();
const parsed = createProjectSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.flatten() },
{ status: 422 }
);
}
// parsed.data is fully typed as CreateProjectInput
const project = await createProject(parsed.data);
return NextResponse.json({ data: project }, { status: 201 });
}
Zod patterns for common fields:
// Reusable schemas
const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
const sortSchema = z.object({
sortBy: z.string().default('created_at'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
const uuidParam = z.string().uuid('Invalid ID format');
// Query params validation (GET routes)
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const query = paginationSchema.merge(sortSchema).safeParse(
Object.fromEntries(searchParams)
);
// ...
}
Step 4: Consistent Response Format
All responses follow a strict structure:
// Success responses — always wrap in { data }
return NextResponse.json({ data: result });
return NextResponse.json({ data: results, meta: { total, page, limit } });
// Error responses — always wrap in { error }
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json(
{ error: 'Validation failed', details: zodError.flatten() },
{ status: 422 }
);
Type definitions for responses:
// types/api.ts
export type ApiResponse = {
data: T;
meta?: {
total: number;
page: number;
limit: number;
};
};
export type ApiError = {
error: string;
details?: unknown;
};
// Helper function
export function apiSuccess(data: T, status = 200): NextResponse {
return NextResponse.json({ data }, { status });
}
export function apiError(error: string, status: number, details?: unknown): NextResponse {
return NextResponse.json({ error, ...(details && { details }) }, { status });
}
Step 5: HTTP Status Codes
Use the correct status code for each situation:
| Code | Meaning | When to Use | |------|---------|------------| | 200 | OK | Successful GET, PATCH, or general success | | 201 | Created | Successful POST that creates a resource | | 204 | No Content | Successful DELETE (no response body) | | 400 | Bad Request | Malformed request (invalid JSON, wrong Content-Type) | | 401 | Unauthorized | Not authenticated (no token, expired session) | | 403 | Forbidden | Authenticated but not authorized (wrong org, wrong role) | | 404 | Not Found | Resource doesn't exist (or user can't see it — use 404 to avoid leaking existence) | | 409 | Conflict | Duplicate resource (unique constraint violation) | | 422 | Unprocessable Entity | Valid JSON but failed validation (Zod errors) | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Unexpected server error (log details, return safe message) |
Key distinction — 401 vs 403 vs 404:
401: "I don't know who you are" → redirect to login403: "I know who you are, but you can't do this" → show permission error404: "This doesn't exist (or you can't know it exists)" → use for privacy-preserving access denial
Step 6: Rate Limiting
Protect public and sensitive endpoints:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '60 s'), // 10 requests per minute
analytics: true,
});
// In route handler
export async function POST(req: NextRequest) {
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded. Try again later.' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
);
}
// Continue with handler...
}
Rate limit tiers:
| Endpoint Type | Limit | Window | |--------------|-------|--------| | Auth (login, register) | 5 requests | 15 minutes | | Public API | 60 requests | 1 minute | | Authenticated API | 120 requests | 1 minute | | Webhooks | 1000 requests | 1 minute | | File upload | 10 requests | 1 hour |
Step 7: Webhook Endpoints
Webhooks require signature verification instead of JWT auth:
import { headers } from 'next/headers';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const body = await req.text(); // Raw body for signature verification
const signature = req.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Process event by type
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Always return 200 quickly — process async if needed
return NextResponse.json({ received: true });
}
Webhook rules:
- Always verify signatures before processing
- Return
200quickly — do heavy processing async - Use
req.text()notreq.json()— signature verification needs raw body - Log unhandled event types (don't error on them — providers add new events)
- Implement idempotency — webhooks can be sent multiple times
Step 8: Generate TypeScript Interfaces
Produce type definitions that match the API contract:
// types/api/organizations.ts
// Request types (match Zod schemas)
export interface CreateOrganizationRequest {
name: string;
slug?: string;
plan?: 'free' | 'starter' | 'professional' | 'enterprise';
}
export interface UpdateOrganizationRequest {
name?: string;
settings?: Partial;
}
// Response types (match database + API transforms)
export interface Organization {
id: string;
name: string;
slug: string;
plan: 'free' | 'starter' | 'professional' | 'enterprise';
createdAt: string; // ISO 8601
updatedAt: string;
}
export interface OrganizationWithMembers extends Organization {
members: OrganizationMember[];
memberCount: number;
}
// List response with pagination
export interface OrganizationListResponse {
data: Organization[];
meta: {
total: number;
page: number;
limit: number;
};
}
Type generation rules:
- Request types match Zod schemas (single source of truth)
- Response types match database models + any API transforms (e.g.,
snake_case→camelCase) - Use
stringfor dates in API types (ISO 8601 format over the wire) - Export all types from a barrel file:
types/api/index.ts
Step 9: Complete Route Handler Template
Full boilerplate for a standard CRUD route:
// app/api/organizations/[orgId]/projects/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { getAuthContext, verifyOrgAccess } from '@/lib/auth';
import { apiSuccess, apiError } from '@/lib/api-response';
import { db } from '@/lib/db';
// --- Validation Schemas ---
const createProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
});
const listQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
status: z.enum(['draft', 'active', 'archived']).optional(),
});
// --- GET /api/organizations/[orgId]/projects ---
export async function GET(
req: NextRequest,
{ params }: { params: { orgId: string } }
) {
try {
const auth = await getAuthContext(req);
if (!auth) return apiError('Authentication required', 401);
const access = await verifyOrgAccess(auth.userId, params.orgId);
if (!access) return apiError('Access denied', 403);
const query = listQuerySchema.safeParse(
Object.fromEntries(new URL(req.url).searchParams)
);
if (!query.success) return apiError('Invalid query', 422, query.error.flatten());
const { page, limit, status } = query.data;
const offset = (page - 1) * limit;
const [projects, total] = await Promise.all([
db.projects.list({ orgId: params.orgId, status, limit, offset }),
db.projects.count({ orgId: params.orgId, status }),
]);
return NextResponse.json({
data: projects,
meta: { total, page, limit },
});
} catch (error) {
console.error('GET /projects failed:', error);
return apiError('Internal server error', 500);
}
}
// --- POST /api/organizations/[orgId]/projects ---
export async function POST(
req: NextRequest,
{ params }: { params: { orgId: string } }
) {
try {
const auth = await getAuthContext(req);
if (!auth) return apiError('Authentication required', 401);
const access = await verifyOrgAccess(auth.userId, params.orgId);
if (!access) return apiError('Access denied', 403);
if (access.role === 'viewer') return apiError('Insufficient permissions', 403);
const body = await req.json();
const parsed = createProjectSchema.safeParse(body);
if (!parsed.success) return apiError('Validation failed', 422, parsed.error.flatten());
const project = await db.projects.create({
...parsed.data,
organizationId: params.orgId,
createdBy: auth.userId,
});
return apiSuccess(project, 201);
} catch (error) {
console.error('POST /projects failed:', error);
return apiError('Internal server error', 500);
}
}
Output summary:
🔌 API Designer — Routes Complete
Feature: [name]
Routes: [count] endpoints across [count] resource groups
Route Table:
Method Path
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [cwinvestments](https://github.com/cwinvestments)
- **Source:** [cwinvestments/memstack](https://github.com/cwinvestments/memstack)
- **License:** MIT
- **Homepage:** https://memstack.pro
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.