api-specifications

Guidance for Splits Network REST API design, implementation, and documentation

16 stars

Best use case

api-specifications is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Guidance for Splits Network REST API design, implementation, and documentation

Teams using api-specifications should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/api-specifications/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/development/api-specifications/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/api-specifications/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How api-specifications Compares

Feature / Agentapi-specificationsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Guidance for Splits Network REST API design, implementation, and documentation

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# API Specifications Skill

This skill provides guidance for designing, implementing, and documenting REST APIs in the Splits Network platform.

## Purpose

Help developers create consistent, well-documented REST APIs that follow Splits Network standards:

- **V2 Architecture**: Standardized 5-route CRUD pattern
- **Response Format**: Consistent `{ data, pagination }` envelope
- **Access Control**: Role-based filtering via shared access context
- **Event Publishing**: Domain events for state changes
- **Documentation**: Clear endpoint specs and examples

## When to Use This Skill

Use this skill when:

- Creating new REST API endpoints
- Migrating V1 endpoints to V2 architecture
- Documenting API contracts
- Implementing pagination, filtering, or search
- Adding validation or error handling
- Publishing domain events

## V2 API Architecture Standards

### 1. Standardized 5-Route Pattern

**Every V2 resource** follows this exact pattern:

```typescript
// 1. LIST - Role-scoped collection
GET /api/v2/:resource?search=X&status=Y&sort_by=Z&page=1&limit=25
Response: { data: [...], pagination: { total, page, limit, total_pages } }

// 2. GET BY ID - Single resource
GET /api/v2/:resource/:id?include=related1,related2
Response: { data: {...} }

// 3. CREATE - New resource
POST /api/v2/:resource
Body: { field1: value1, field2: value2, ... }
Response: { data: {...} }

// 4. UPDATE - Single method handles ALL updates
PATCH /api/v2/:resource/:id
Body: { field1: newValue1, status: newStatus, ... }
Response: { data: {...} }

// 5. DELETE - Soft delete
DELETE /api/v2/:resource/:id
Response: { data: { message: 'Deleted successfully' } }
```

### 2. Response Format Standard

**ALL responses MUST use the wrapped envelope**:

```typescript
// Success response
reply.send({ data: <payload> })

// List response with pagination
reply.send({
  data: [...],
  pagination: { total, page, limit, total_pages }
})

// Error response
reply.code(400).send({
  error: { code: "ERROR_CODE", message: "..." }
})
```

**NEVER return unwrapped data**: `reply.send(payload)` is incorrect.

### 3. Domain-Based Folder Structure

```
services/<service>/src/v2/
├── shared/                 # Shared V2 utilities
│   ├── events.ts           # EventPublisher class
│   ├── helpers.ts          # requireUserContext, validation
│   └── pagination.ts       # PaginationParams, PaginationResponse
├── <domain>/               # Domain folder (e.g., jobs, candidates)
│   ├── types.ts            # Domain-specific types
│   ├── repository.ts       # Data access with role-based filtering
│   └── service.ts          # Business logic, validation, events
└── routes.ts               # All V2 routes (imports from domains)
```

### 4. Repository Pattern with Access Context

```typescript
import { resolveAccessContext } from "@splits-network/shared-access-context";

export class ResourceRepository {
    constructor(private supabase: SupabaseClient) {}

    async list(clerkUserId: string, filters: ResourceFilters) {
        const context = await resolveAccessContext(clerkUserId, this.supabase);

        const query = this.supabase.from("resources").select("*");

        // Apply role-based filtering from access context
        if (context.role === "recruiter") {
            query.eq("user_id", context.userId);
        } else if (context.role === "company_admin") {
            query.in("company_id", context.accessibleCompanyIds);
        }
        // Platform admins see everything (no filter)

        // Apply search/sorting filters
        if (filters.search) {
            query.ilike("name", `%${filters.search}%`);
        }
        if (filters.sort_by) {
            query.order(filters.sort_by, {
                ascending: filters.sort_order !== "desc",
            });
        }

        return query;
    }
}
```

### 5. Service Layer with Events

```typescript
export class ResourceServiceV2 {
    constructor(
        private repository: ResourceRepository,
        private events: EventPublisher,
    ) {}

    async create(clerkUserId: string, data: ResourceCreate) {
        // Validate input
        this.validateResourceData(data);

        // Create via repository
        const resource = await this.repository.create(clerkUserId, data);

        // Publish event after successful creation
        await this.events.publish("resource.created", {
            resourceId: resource.id,
            createdBy: clerkUserId,
        });

        return resource;
    }

    async update(id: string, clerkUserId: string, updates: ResourceUpdate) {
        // Smart validation based on what's being updated
        if (updates.status) {
            this.validateStatusTransition(updates.status);
        }

        const updated = await this.repository.update(id, clerkUserId, updates);

        // Publish event
        await this.events.publish("resource.updated", {
            resourceId: id,
            changes: Object.keys(updates),
            updatedBy: clerkUserId,
        });

        return updated;
    }
}
```

### 6. Route Implementation

```typescript
import { FastifyInstance } from "fastify";

export async function resourceRoutes(
    app: FastifyInstance,
    service: ResourceServiceV2,
) {
    // LIST
    app.get("/api/v2/resources", async (request, reply) => {
        const clerkUserId = request.headers["x-clerk-user-id"] as string;
        const {
            search,
            status,
            sort_by,
            sort_order,
            page = 1,
            limit = 25,
        } = request.query as any;

        const result = await service.list(clerkUserId, {
            search,
            status,
            sort_by,
            sort_order,
            page,
            limit,
        });

        return reply.send(result); // Service returns { data, pagination }
    });

    // GET BY ID
    app.get("/api/v2/resources/:id", async (request, reply) => {
        const clerkUserId = request.headers["x-clerk-user-id"] as string;
        const { id } = request.params as { id: string };
        const { include } = request.query as { include?: string };

        const resource = await service.getById(id, clerkUserId, include);
        return reply.send({ data: resource });
    });

    // CREATE
    app.post("/api/v2/resources", async (request, reply) => {
        const clerkUserId = request.headers["x-clerk-user-id"] as string;
        const data = request.body as ResourceCreate;

        const resource = await service.create(clerkUserId, data);
        return reply.code(201).send({ data: resource });
    });

    // UPDATE
    app.patch("/api/v2/resources/:id", async (request, reply) => {
        const clerkUserId = request.headers["x-clerk-user-id"] as string;
        const { id } = request.params as { id: string };
        const updates = request.body as ResourceUpdate;

        const resource = await service.update(id, clerkUserId, updates);
        return reply.send({ data: resource });
    });

    // DELETE
    app.delete("/api/v2/resources/:id", async (request, reply) => {
        const clerkUserId = request.headers["x-clerk-user-id"] as string;
        const { id } = request.params as { id: string };

        await service.delete(id, clerkUserId);
        return reply.send({ data: { message: "Deleted successfully" } });
    });
}
```

## Query Parameters

### List Endpoints

Standard query parameters for list endpoints:

```typescript
interface StandardListParams {
    page?: number; // Page number (1-based)
    limit?: number; // Items per page (default 25, max 100)
    search?: string; // Search term (service-specific fields)
    sort_by?: string; // Field to sort by
    sort_order?: "asc" | "desc"; // Sort direction
    filters?: Record<string, any>; // Domain-specific filters
    include?: string; // Comma-separated related resources
}
```

**Examples**:

- `/api/v2/jobs?page=1&limit=25&search=engineer&status=active`
- `/api/v2/applications?candidate_id=123&stage=screen&sort_by=created_at&sort_order=desc`
- `/api/v2/candidates?include=documents,applications`

### Get By ID Endpoints

Support `include` parameter for related data:

```typescript
// GET /api/v2/applications/:id?include=candidate,job,ai_review
if (includes.includes("candidate")) {
    application.candidate = await this.getCandidateData(
        application.candidate_id,
    );
}
```

## Pagination

All list endpoints must return pagination metadata:

```typescript
interface PaginationResponse {
  total: number;        // Total items across all pages
  page: number;         // Current page number (1-based)
  limit: number;        // Items per page
  total_pages: number;  // Total number of pages
}

// Example response
{
  "data": [...],
  "pagination": {
    "total": 1000,
    "page": 1,
    "limit": 25,
    "total_pages": 40
  }
}
```

## Error Handling

Return structured error responses:

```typescript
// 400 Bad Request - Validation error
reply.code(400).send({
    error: {
        code: "VALIDATION_ERROR",
        message: "Invalid input data",
        details: { field: "email", reason: "Invalid email format" },
    },
});

// 401 Unauthorized - Missing/invalid auth
reply.code(401).send({
    error: {
        code: "UNAUTHORIZED",
        message: "Authentication required",
    },
});

// 403 Forbidden - Insufficient permissions
reply.code(403).send({
    error: {
        code: "FORBIDDEN",
        message: "Insufficient permissions to access this resource",
    },
});

// 404 Not Found - Resource doesn't exist
reply.code(404).send({
    error: {
        code: "NOT_FOUND",
        message: "Resource not found",
        details: { resourceId: id },
    },
});

// 409 Conflict - Duplicate or constraint violation
reply.code(409).send({
    error: {
        code: "CONFLICT",
        message: "Resource already exists",
        details: { constraint: "unique_email" },
    },
});

// 500 Internal Server Error - Unexpected error
reply.code(500).send({
    error: {
        code: "INTERNAL_ERROR",
        message: "An unexpected error occurred",
    },
});
```

## Event Publishing

Publish domain events for significant state changes:

```typescript
// After successful create
await this.events.publish("resource.created", {
    resourceId: resource.id,
    companyId: resource.company_id,
    createdBy: clerkUserId,
});

// After successful update
await this.events.publish("resource.updated", {
    resourceId: id,
    changes: Object.keys(updates),
    updatedBy: clerkUserId,
});

// After successful delete
await this.events.publish("resource.deleted", {
    resourceId: id,
    deletedBy: clerkUserId,
});

// Domain-specific events
await this.events.publish("application.stage_changed", {
    applicationId: id,
    oldStage: oldStage,
    newStage: newStage,
    changedBy: clerkUserId,
});
```

## API Documentation

Document each endpoint with:

1. **Purpose**: What the endpoint does
2. **Path**: Full path with parameters
3. **Method**: HTTP method (GET, POST, PATCH, DELETE)
4. **Auth**: Required authentication (Clerk JWT)
5. **Access**: Who can access (roles, scoping rules)
6. **Query Params**: Optional parameters
7. **Request Body**: Expected payload structure
8. **Response**: Success response format
9. **Errors**: Possible error codes and meanings
10. **Example**: Full request/response example

### Example Documentation

````markdown
### List Jobs

Returns a paginated list of jobs based on role-based access control.

**Endpoint**: `GET /api/v2/jobs`

**Authentication**: Required (Clerk JWT)

**Access Control**:

- **Recruiters**: See assigned jobs only
- **Company Users**: See jobs from their organization
- **Platform Admins**: See all jobs

**Query Parameters**:

- `page` (number, optional): Page number (default: 1)
- `limit` (number, optional): Items per page (default: 25, max: 100)
- `search` (string, optional): Search in job title
- `status` (string, optional): Filter by status (active, paused, closed)
- `company_id` (uuid, optional): Filter by company
- `sort_by` (string, optional): Sort field (created_at, title)
- `sort_order` (string, optional): Sort direction (asc, desc)

**Response**: `200 OK`

```json
{
    "data": [
        {
            "id": "123e4567-e89b-12d3-a456-426614174000",
            "title": "Senior Software Engineer",
            "company_id": "123e4567-e89b-12d3-a456-426614174001",
            "status": "active",
            "location": "San Francisco, CA",
            "created_at": "2026-01-13T10:00:00Z"
        }
    ],
    "pagination": {
        "total": 100,
        "page": 1,
        "limit": 25,
        "total_pages": 4
    }
}
```
````

**Errors**:

- `401 Unauthorized`: Missing or invalid authentication
- `403 Forbidden`: Insufficient permissions
- `500 Internal Server Error`: Server error

**Example Request**:

```bash
curl -X GET "https://api.splits.network/api/v2/jobs?page=1&limit=25&status=active" \
  -H "Authorization: Bearer <clerk-jwt>"
```

````

## Common Patterns

### Current User Access Pattern

For user-specific singleton resources, use the `/me` alias pattern:

```typescript
// ✅ RECOMMENDED - Use /me alias on existing GET by ID endpoint
GET /api/v2/candidates/me
// Resolves to user's actual ID, returns { data: {...} }

// Implementation in route handler
app.get('/api/v2/candidates/:id', async (request, reply) => {
  const clerkUserId = request.headers['x-clerk-user-id'] as string;
  let { id } = request.params;

  // Resolve "me" to actual user ID
  if (id === 'me') {
    const context = await resolveAccessContext(clerkUserId, supabase);
    id = context.userId; // Actual UUID
  }

  // Standard getById logic - no special handling needed
  const candidate = await service.getById(id, clerkUserId);
  return reply.send({ data: candidate });
});
````

**Benefits**:

- **Intuitive**: Clear what `/me` means
- **Performant**: Direct ID lookup (not filtered list query)
- **Correct Shape**: Returns `{ data: {...} }` (singleton, not array)
- **No New Endpoint**: Just an alias within existing GET by ID route
- **Still Secure**: Access context validates user can access resolved ID

### Single Update Method

One update method handles ALL updates with smart validation:

```typescript
// ❌ WRONG - Multiple update endpoints
PATCH /api/v2/jobs/:id/status
PATCH /api/v2/jobs/:id/title
PATCH /api/v2/jobs/:id/close

// ✅ CORRECT - Single update endpoint
PATCH /api/v2/jobs/:id
Body: { status: "closed" } or { title: "New Title" } or any field
```

### Include Parameters vs Child Endpoints

Use include parameters for related data, NOT child endpoints:

```typescript
// ❌ WRONG - Child endpoints
GET /api/v2/applications/:id/documents
GET /api/v2/applications/:id/ai-review

// ✅ CORRECT - Include parameters
GET /api/v2/applications/:id?include=documents,ai_review
GET /api/v2/documents?application_id=:id
GET /api/v2/ai-reviews?application_id=:id
```

### Data Enrichment with JOINs

Services share the same database (all tables in `public` schema) and can enrich data with JOINs:

```typescript
// Enrich applications with candidate and job data
const enrichedApplications = await this.supabase.from("applications").select(`
    *,
    candidate:candidates(*),
    job:jobs(*),
    recruiter:recruiters(*)
  `);

// Note: All domain tables (applications, candidates, jobs, recruiters, etc.)
// are in the public schema. No cross-schema queries needed.
```

## References

- **API Response Format**: `docs/guidance/api-response-format.md`
- **Pagination Standard**: `docs/guidance/pagination.md`
- **V2 Architecture Guide**: `docs/migration/v2/V2-ARCHITECTURE-IMPLEMENTATION-GUIDE.md`
- **Service Architecture**: `docs/guidance/service-architecture-pattern.md`
- **Access Context**: `packages/shared-access-context/README.md`

## Examples from Production

**V2 Services**:

- Identity Service: `services/identity-service/src/v2/`
- ATS Service: `services/ats-service/src/v2/`
- Network Service: `services/network-service/src/v2/`
- Billing Service: `services/billing-service/src/v2/`
- Notification Service: `services/notification-service/src/v2/`

**API Gateway Routes**:

- Gateway V2 Proxy: `services/api-gateway/src/routes/v2/`

Related Skills

bgo

10
from diegosouzapw/awesome-omni-skill

Automates the complete Blender build-go workflow, from building and packaging your extension/add-on to removing old versions, installing, enabling, and launching Blender for quick testing and iteration.

Coding & Development

testing-builder

16
from diegosouzapw/awesome-omni-skill

Automatically generates comprehensive test suites (unit, integration, E2E) based on code and past testing patterns. Use when user says "write tests", "test this", "add coverage", or after fixing bugs to create regression tests. Eliminates testing friction for ADHD users.

Testing Anti-Patterns

16
from diegosouzapw/awesome-omni-skill

This skill should be used when encountering "flaky tests", "test maintenance issues", "slow test suites", "brittle tests", "test code smells", "test debugging problems", or when tests are hard to understand, maintain, or debug.

testcontainers-integration-tests

16
from diegosouzapw/awesome-omni-skill

Use when integration tests require real infrastructure (database, message queue, cache) or when mocking infrastructure is insufficient. Defines container lifecycle, test isolation, and performance optimization for Testcontainers-based testing.

test-generation

16
from diegosouzapw/awesome-omni-skill

Master skill for intelligent test case generation. Identifies technology stack and delegates to specialized testing sub-skills for creating high-quality integration and API tests.

test-driven-development

16
from diegosouzapw/awesome-omni-skill

Use when implementing any feature or bugfix, before writing implementation code

Test and Refine Your Kubernetes Skill

16
from diegosouzapw/awesome-omni-skill

No description provided.

test-and-fix-workflow

16
from diegosouzapw/awesome-omni-skill

Automated workflow for running tests and fixing failures systematically. Use when implementing the mandatory test workflow or fixing code quality issues. Keywords - testing, debugging, workflow, failures, systematic fixes.

terway-troubleshooting

16
from diegosouzapw/awesome-omni-skill

Troubleshoot Terway CNI issues in Kubernetes using Kubernetes events and Terway logs. Use when diagnosing "cni plugin not initialized", Pod create/delete failures, or ENI/IPAM problems in Terway (centralized or non-centralized IPAM).

Tero Voice Project Context

16
from diegosouzapw/awesome-omni-skill

Load full project context, tech stack, status, and guidelines for the AI Receptionist SaaS project

temporal-python-pro

16
from diegosouzapw/awesome-omni-skill

Master Temporal workflow orchestration with Python SDK. Implements durable workflows, saga patterns, and distributed transactions. Covers async/await, testing strategies, and production deployment.

temporal-golang-pro

16
from diegosouzapw/awesome-omni-skill

Use when building durable distributed systems with Temporal Go SDK. Covers deterministic workflow rules, mTLS worker configs, and advanced patterns.