agentsclimarketplace

Api scaffold

Skill xmqywx/claude-code-skills/skills/api-scaffold

17 professional Claude Code skills - code review, API scaffold, n8n workflow generator, and more

Install
npx -y skills add xmqywx/claude-code-skills --skill api-scaffold

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Scaffold a production-ready REST API endpoint with CRUD operations, input validation, error handling, rate limiting, and test skeletons. Supports Express and Fastify.

SKILL.md

10.8 KB, as published. Nobody here has run it

API Scaffold

Scaffold a complete REST API resource with CRUD endpoints, validation, error handling, and tests.

Inputs

When invoked, determine these from the user's request:

  • Resource name (required): e.g., "user", "product", "order". Singularize if plural.
  • Framework (optional, default: auto-detect from package.json): "express" or "fastify".
  • Language (optional, default: auto-detect from project): "typescript" or "javascript".
  • Fields (optional): If the user specifies fields, use them. Otherwise, create a sensible default schema with id, createdAt, updatedAt, and 2-3 domain-relevant fields.

Steps

1. Detect Project Setup

Run these commands to understand the project:

cat package.json
ls src/ || ls app/ || ls .

Determine:

  • Framework: Check dependencies for express, fastify, @fastify/, koa, etc.
  • Language: Check for tsconfig.json, file extensions in src/.
  • Project structure: Identify where routes/controllers live (e.g., src/routes/, src/api/, routes/).
  • Existing patterns: Read one existing route file to match the project's conventions.

If no package.json exists, ask the user which framework and language to use.

2. Create the Validation Schema

Create <resource>.schema.<ext> in the appropriate directory.

TypeScript + Zod example (src/schemas/product.schema.ts):

import { z } from 'zod';

export const createProductSchema = z.object({
  name: z.string().min(1).max(255),
  description: z.string().max(2000).optional(),
  price: z.number().positive().finite(),
  category: z.string().min(1).max(100),
  inStock: z.boolean().default(true),
});

export const updateProductSchema = createProductSchema.partial();

export const productIdSchema = z.object({
  id: z.string().uuid(),
});

export const listProductsQuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['createdAt', 'name', 'price']).default('createdAt'),
  order: z.enum(['asc', 'desc']).default('desc'),
  search: z.string().max(255).optional(),
});

export type CreateProductInput = z.infer<typeof createProductSchema>;
export type UpdateProductInput = z.infer<typeof updateProductSchema>;
export type ListProductsQuery = z.infer<typeof listProductsQuerySchema>;

If the project uses Joi instead of Zod, use Joi syntax. If neither is installed, use Zod and note that the user should npm install zod.

3. Create the Route/Controller File

Create the route file following the detected project structure.

Express pattern (src/routes/product.routes.<ext>):

import { Router, Request, Response, NextFunction } from 'express';

const router = Router();

// GET /api/products - List with pagination, sorting, search
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const query = listProductsQuerySchema.parse(req.query);
    // TODO: Replace with actual data source
    const { page, limit, sort, order, search } = query;
    const offset = (page - 1) * limit;
    const items: any[] = []; // TODO: query database
    const total = 0; // TODO: count query
    res.json({
      data: items,
      meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
    });
  } catch (err) { next(err); }
});

// GET /api/products/:id - Get single resource
router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { id } = productIdSchema.parse(req.params);
    const item = null; // TODO: find by id
    if (!item) { res.status(404).json({ error: 'Product not found' }); return; }
    res.json({ data: item });
  } catch (err) { next(err); }
});

// POST /api/products - Create resource
router.post('/', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const data = createProductSchema.parse(req.body);
    const item = null; // TODO: insert into database
    res.status(201).json({ data: item });
  } catch (err) { next(err); }
});

// PUT /api/products/:id - Update resource
router.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { id } = productIdSchema.parse(req.params);
    const data = updateProductSchema.parse(req.body);
    const item = null; // TODO: update in database
    if (!item) { res.status(404).json({ error: 'Product not found' }); return; }
    res.json({ data: item });
  } catch (err) { next(err); }
});

// DELETE /api/products/:id - Delete resource
router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { id } = productIdSchema.parse(req.params);
    const deleted = false; // TODO: delete from database
    if (!deleted) { res.status(404).json({ error: 'Product not found' }); return; }
    res.status(204).send();
  } catch (err) { next(err); }
});

export default router;

Fastify pattern (src/routes/product.routes.<ext>):

import { FastifyInstance } from 'fastify';

export default async function productRoutes(app: FastifyInstance) {
  app.get('/', { schema: { querystring: listSchema } }, async (req, reply) => {
    // ... pagination + list logic
  });
  app.get('/:id', async (req, reply) => { /* ... */ });
  app.post('/', async (req, reply) => { /* ... */ });
  app.put('/:id', async (req, reply) => { /* ... */ });
  app.delete('/:id', async (req, reply) => { /* ... */ });
}

4. Create Error Handling Middleware

If the project does not already have centralized error handling, create src/middleware/errorHandler.<ext>:

import { Request, Response, NextFunction } from 'express';
import { ZodError } from 'zod';

export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
  // Validation errors
  if (err instanceof ZodError) {
    res.status(400).json({
      error: 'Validation failed',
      details: err.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message,
      })),
    });
    return;
  }

  // Known application errors
  if ('statusCode' in err && typeof (err as any).statusCode === 'number') {
    res.status((err as any).statusCode).json({ error: err.message });
    return;
  }

  // Unknown errors - do not leak internals
  console.error('[ERROR]', err);
  res.status(500).json({ error: 'Internal server error' });
}

5. Create Rate Limiting Middleware

If not already present, create src/middleware/rateLimit.<ext>:

const requestCounts = new Map<string, { count: number; resetAt: number }>();

export function rateLimit({ windowMs = 60_000, max = 100 } = {}) {
  return (req: any, res: any, next: any) => {
    const key = req.ip || req.connection?.remoteAddress || 'unknown';
    const now = Date.now();
    const record = requestCounts.get(key);

    if (!record || now > record.resetAt) {
      requestCounts.set(key, { count: 1, resetAt: now + windowMs });
      return next();
    }

    record.count++;
    res.setHeader('X-RateLimit-Limit', String(max));
    res.setHeader('X-RateLimit-Remaining', String(Math.max(0, max - record.count)));

    if (record.count > max) {
      res.status(429).json({ error: 'Too many requests. Please try again later.' });
      return;
    }

    next();
  };
}

Note: For production, recommend express-rate-limit or @fastify/rate-limit instead of this in-memory implementation.

6. Create Test Skeleton

Create <resource>.test.<ext> in the test directory (or alongside the route if no test dir exists):

import { describe, it, expect, beforeAll, afterAll } from 'vitest'; // or jest
// import supertest if available

const BASE_URL = '/api/products';

describe('Product API', () => {
  describe('POST /api/products', () => {
    it('should create a product with valid data', async () => {
      // TODO: send POST request with valid body
      // expect status 201
      // expect response.data to match input
    });

    it('should return 400 for invalid data', async () => {
      // TODO: send POST with missing required fields
      // expect status 400
      // expect response.error to be 'Validation failed'
    });
  });

  describe('GET /api/products', () => {
    it('should return paginated list', async () => {
      // TODO: send GET request
      // expect status 200
      // expect response.data to be an array
      // expect response.meta to have page, limit, total, totalPages
    });

    it('should respect pagination params', async () => {
      // TODO: send GET with ?page=2&limit=5
      // verify correct offset behavior
    });
  });

  describe('GET /api/products/:id', () => {
    it('should return a product by ID', async () => {
      // TODO: create a product first, then fetch by ID
      // expect status 200
    });

    it('should return 404 for non-existent ID', async () => {
      // TODO: fetch with random UUID
      // expect status 404
    });
  });

  describe('PUT /api/products/:id', () => {
    it('should update an existing product', async () => {
      // TODO: create, then update
      // expect status 200
      // expect updated fields
    });

    it('should allow partial updates', async () => {
      // TODO: send PUT with only one field
      // expect other fields unchanged
    });
  });

  describe('DELETE /api/products/:id', () => {
    it('should delete an existing product', async () => {
      // TODO: create, then delete
      // expect status 204
    });

    it('should return 404 for non-existent ID', async () => {
      // TODO: delete with random UUID
      // expect status 404
    });
  });
});

7. Show Registration Instructions

After creating all files, show the user how to register the new route in their app entry point:

// In your main app file (e.g., src/app.ts or src/index.ts):
import productRoutes from './routes/product.routes';
app.use('/api/products', productRoutes);

8. Summary

Print a summary of all created files and any packages that need to be installed:

Created:
  - src/schemas/product.schema.ts
  - src/routes/product.routes.ts
  - src/middleware/errorHandler.ts (if new)
  - src/middleware/rateLimit.ts (if new)
  - tests/product.test.ts

Install (if needed):
  npm install zod
  npm install -D vitest supertest @types/supertest

Next steps:
  1. Register the route in your app entry point
  2. Replace TODO comments with actual database logic
  3. Run tests: npx vitest product

Notes

  • Always match existing project conventions (naming, file structure, import style, semicolons, quotes).
  • If the project uses ESM ("type": "module"), use import/export. If CJS, use require/module.exports.
  • Do not overwrite existing files. If a route file already exists, warn the user and ask before proceeding.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.