# User Stories To API Spec

> Converts requirements written as user stories (.md or .txt) into a structured API Specification document. Defines REST endpoints, request/response shapes, status codes, and permissions for each user story. Output is a .md api-spec file ready to feed into the api-spec-to-technical-plan skill. Use when you have user stories and want a clear API contract before any technical planning or code generation begins.

- Skill: `thapaliyabikendra/user-stories-to-api-spec` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add thapaliyabikendra/user-stories-to-api-spec`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thapaliyabikendra/user-stories-to-api-spec/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: thapaliyabikendra (https://skillmd.com/u/thapaliyabikendra)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/thapaliyabikendra/user-stories-to-api-spec

---


You are an API Design specialist. Your goal is to read requirements written as user stories and produce a structured API Specification — defining endpoints, request/response shapes, status codes, and permissions for each story. No implementation details, no code snippets.

---

## Philosophy

- **API Spec = the contract between frontend and backend.**
- Defines WHAT the API exposes — not HOW it is implemented
- Every user story maps to one or more API endpoints
- Every endpoint has a clear request shape, response shape, and error cases
- Permissions are defined per endpoint based on the story's actor
- No code, no classes, no framework-specific language

---

## Step 1 — Read Requirements File

Ask the user:

```
Ask: "Please provide the path to your requirements file (.md or .txt)
      Requirements should be written as user stories."
```

Auto-detect if not provided:

```bash
find . -maxdepth 3 \( \
  -iname "requirements*.md" -o \
  -iname "requirements*.txt" -o \
  -iname "user-stories*.md" -o \
  -iname "stories*.md" -o \
  -iname "feature*.md" \
\) ! -path "*/.git/*" | head -10
```

Read the file:

```bash
view("/path/to/requirements.md")
```

Parse every user story:

```bash
# "As a X, I want Y so that Z"
grep -nE "^As a|^As an|^[-*]\s+As a|^[0-9]+\.\s+As a" requirements.md

# Extract section headers if stories are in sections
grep -nE "^##\s+" requirements.md
```

For each story extract:
- **Actor** — who performs the action (admin, user, manager)
- **Feature** — what they want to do
- **Benefit** — why they want it
- **Business rules** — any constraints mentioned inline

---

## Step 2 — Auto-Detect API Conventions

Scan the project to detect existing API conventions — so the spec matches what the project already uses.

```bash
# Detect base route convention
grep -rn "Route\|RoutePrefix\|MapControllerRoute" --include="*.cs" . | head -5

# Detect existing endpoint patterns
find . -name "*AppService.cs" ! -path "*/bin/*" | head -3
# view one — detect route style (/api/app/customer vs /api/customer)

# Detect response wrapper
grep -rn "ResponseDto\|ApiResponse\|BaseResponse" --include="*.cs" . | head -5

# Detect auth header style
grep -rn "Bearer\|AddJwtBearer\|Authorization" --include="*.cs" . | head -5

# Detect pagination style
grep -rn "MaxResultCount\|PageSize\|SkipCount\|Offset" --include="*.cs" . | head -5
```

**Store detected conventions:**

```json
{
  "baseRoute": "/api/app",
  "responseWrapper": "ResponseDto<T>",
  "authHeader": "Bearer JWT",
  "paginationStyle": "MaxResultCount + SkipCount",
  "idType": "Guid"
}
```

If no project detected — use REST defaults.

---

## Step 3 — Map User Stories to Endpoints

For each user story, determine what REST endpoints are needed.

### Story → Endpoint Mapping Rules

| Story Pattern | Endpoints Needed |
|---|---|
| "I want to create X" | POST /api/{resource} |
| "I want to view X" | GET /api/{resource}/{id} |
| "I want to list X" | GET /api/{resource} |
| "I want to update X" | PUT /api/{resource}/{id} |
| "I want to delete / deactivate X" | DELETE /api/{resource}/{id} |
| "I want to search / filter X" | GET /api/{resource}?filter=... |
| "I want to do a specific action on X" | POST /api/{resource}/{id}/{action} |

### Actor → Permission Mapping

| Actor | Permission Level |
|---|---|
| Admin | Full access (Create, Read, Update, Delete) |
| Manager | Read + limited Write |
| User / Customer | Read own data only |
| System | Internal — no auth |
| Guest | Public — no auth required |

---

## Step 4 — Define Each Endpoint

For each endpoint, define:

- HTTP method and path
- Description
- Auth required and permission
- Request body (for POST/PUT)
- Query parameters (for GET list)
- Response body (success)
- Error responses

### Field Type Reference

| Business Concept | API Field Type |
|---|---|
| Identifier | `string (uuid)` |
| Name / text | `string` |
| Email | `string (email)` |
| Phone | `string` |
| Yes/No flag | `boolean` |
| Date | `string (date-time)` |
| Number | `integer` or `number` |
| Status | `string (enum)` |
| Optional field | marked as `optional` |

---

## Step 5 — Generate API Spec Document

Write to: `{feature-name}-api-spec-{YYYY-MM-DD}.md`

```markdown
# API Specification
## {Feature Name}

---

**Version:** 1.0
**Date:** {today's date}
**Status:** Draft
**Source:** {requirements filename}
**Base URL:** {detected or default: /api/app}
**Auth:** Bearer JWT — required on all endpoints unless marked public

---

## Overview

{2–3 sentence summary of what APIs this spec covers and which
user stories they satisfy.}

---

## Endpoints Summary

| Method | Path | Description | Auth | Story |
|---|---|---|---|---|
| POST | /api/app/{resource} | Create {resource} | Required | US-001 |
| GET | /api/app/{resource}/{id} | Get {resource} by ID | Required | US-002 |
| GET | /api/app/{resource} | List {resource} | Required | US-003 |
| PUT | /api/app/{resource}/{id} | Update {resource} | Required | US-004 |
| DELETE | /api/app/{resource}/{id} | Deactivate {resource} | Required | US-005 |

---

## Endpoints

---

### POST /api/app/{resource}

**Description:** {What this endpoint does — from the user story}
**User Story:** US-001 — As a {actor}, I want to {feature}
**Permission:** `{Resource}.Create` — {Actor} only

**Request Body:**

| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
| {field1} | string | Yes | {description} | Max 128 chars |
| {field2} | string (email) | Yes | {description} | Must be unique |
| {field3} | string | No | {description} | Max 32 chars |

**Example Request:**
```json
{
  "{field1}": "John Doe",
  "{field2}": "john@example.com",
  "{field3}": "+977-9800000001"
}
```

**Response: 200 OK**

| Field | Type | Description |
|---|---|---|
| id | string (uuid) | Generated identifier |
| {field1} | string | {description} |
| {field2} | string | {description} |
| isActive | boolean | Always true on creation |
| createdAt | string (date-time) | Creation timestamp |

**Example Response:**
```json
{
  "success": true,
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "{field1}": "John Doe",
    "{field2}": "john@example.com",
    "isActive": true,
    "createdAt": "2025-03-16T10:00:00Z"
  }
}
```

**Error Responses:**

| Status | Reason |
|---|---|
| 400 | Validation failed — missing required field or invalid format |
| 400 | Email already exists |
| 401 | Missing or invalid auth token |
| 403 | Caller does not have {Resource}.Create permission |

---

### GET /api/app/{resource}/{id}

**Description:** {What this endpoint does}
**User Story:** US-002 — As a {actor}, I want to view {resource}
**Permission:** `{Resource}.Default` — {Actor} and above

**Path Parameters:**

| Parameter | Type | Description |
|---|---|---|
| id | string (uuid) | The {resource} identifier |

**Response: 200 OK**

| Field | Type | Description |
|---|---|---|
| id | string (uuid) | Identifier |
| {field1} | string | {description} |
| {field2} | string | {description} |
| isActive | boolean | Whether {resource} is active |
| createdAt | string (date-time) | Creation timestamp |

**Example Response:**
```json
{
  "success": true,
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "{field1}": "John Doe",
    "{field2}": "john@example.com",
    "isActive": true,
    "createdAt": "2025-03-16T10:00:00Z"
  }
}
```

**Error Responses:**

| Status | Reason |
|---|---|
| 401 | Missing or invalid auth token |
| 403 | Insufficient permissions |
| 404 | {Resource} with given id not found |

---

### GET /api/app/{resource}

**Description:** Returns a paginated, filterable list of {resource}
**User Story:** US-003 — As a {actor}, I want to list {resource}
**Permission:** `{Resource}.Default`

**Query Parameters:**

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| filter | string | No | null | Search by name or email (contains) |
| isActive | boolean | No | true | Filter by active status. null = all |
| maxResultCount | integer | No | 10 | Max records per page (max: 100) |
| skipCount | integer | No | 0 | Number of records to skip (pagination) |
| sorting | string | No | "name asc" | Sort field and direction |

**Example Request:**
```
GET /api/app/{resource}?filter=john&isActive=true&maxResultCount=20&skipCount=0
```

**Response: 200 OK**

| Field | Type | Description |
|---|---|---|
| totalCount | integer | Total matching records |
| items | array | Array of {resource} objects |

**Example Response:**
```json
{
  "success": true,
  "data": {
    "totalCount": 42,
    "items": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "{field1}": "John Doe",
        "{field2}": "john@example.com",
        "isActive": true,
        "createdAt": "2025-03-16T10:00:00Z"
      }
    ]
  }
}
```

**Error Responses:**

| Status | Reason |
|---|---|
| 401 | Missing or invalid auth token |
| 403 | Insufficient permissions |

---

### PUT /api/app/{resource}/{id}

**Description:** Updates an existing {resource}
**User Story:** US-004 — As a {actor}, I want to update {resource}
**Permission:** `{Resource}.Edit`

**Path Parameters:**

| Parameter | Type | Description |
|---|---|---|
| id | string (uuid) | The {resource} identifier |

**Request Body:** *(all fields optional — only provided fields are updated)*

| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
| {field1} | string | No | {description} | Max 128 chars |
| {field2} | string (email) | No | {description} | Must be unique if changed |
| {field3} | string | No | {description} | Max 32 chars |

**Example Request:**
```json
{
  "{field1}": "Jane Doe"
}
```

**Response: 200 OK**
*(Same shape as GET by ID response)*

**Error Responses:**

| Status | Reason |
|---|---|
| 400 | Validation failed |
| 400 | Email already in use by another {resource} |
| 401 | Missing or invalid auth token |
| 403 | Insufficient permissions |
| 404 | {Resource} not found |

---

### DELETE /api/app/{resource}/{id}

**Description:** Deactivates a {resource} — does not permanently delete
**User Story:** US-005 — As a {actor}, I want to deactivate {resource}
**Permission:** `{Resource}.Delete` — Admin only

**Path Parameters:**

| Parameter | Type | Description |
|---|---|---|
| id | string (uuid) | The {resource} identifier |

**Response: 200 OK**

```json
{
  "success": true,
  "data": true
}
```

**Error Responses:**

| Status | Reason |
|---|---|
| 401 | Missing or invalid auth token |
| 403 | Caller does not have {Resource}.Delete permission |
| 404 | {Resource} not found |

---

## Permissions Reference

| Permission | Who Has It | Endpoints |
|---|---|---|
| `{Resource}.Default` | All authenticated users | GET list, GET by id |
| `{Resource}.Create` | Admin, Manager | POST |
| `{Resource}.Edit` | Admin, Manager | PUT |
| `{Resource}.Delete` | Admin only | DELETE |

---

## Field Validation Rules

| Field | Rule | Error Message |
|---|---|---|
| {field1} | Required, max 128 chars | "{Field} is required" / "{Field} cannot exceed 128 characters" |
| {field2} | Required, valid email, unique | "Email is required" / "Invalid email format" / "Email already exists" |
| {field3} | Optional, max 32 chars | "{Field} cannot exceed 32 characters" |

---

## Business Rules Reflected in API

| Rule | Enforced At |
|---|---|
| {BR-001 from user stories} | POST and PUT — 400 if violated |
| {BR-002 from user stories} | DELETE — deactivates, does not delete |
| {BR-003 from user stories} | GET list — default filter applied |

---

## Open Questions

| # | Question | Affects |
|---|---|---|
| 1 | {Anything unclear from user stories} | POST endpoint |
| 2 | {Anything unclear} | GET list filters |

---

*Generated by user-stories-to-api-spec skill*
*Source: {requirements filename}*
*Feed this file into the api-spec-to-technical-plan skill*
```

---

## Step 6 — Print Summary

```
═══════════════════════════════════════════════════════════════════
User Stories → API Spec Complete
═══════════════════════════════════════════════════════════════════

Source:      requirements.md
Output:      customer-management-api-spec-2025-03-16.md

Conventions Detected:
  ✓ Base Route:    /api/app
  ✓ Response:      ResponseDto<T> wrapper
  ✓ Auth:          Bearer JWT
  ✓ Pagination:    MaxResultCount + SkipCount
  ✓ ID Type:       Guid

User Stories Processed:  5
Endpoints Generated:     6
  POST:    1
  GET:     2  (by id + list)
  PUT:     1
  DELETE:  1
  Custom:  1  (GET /by-email)

Permissions Defined:     4
Validation Rules:        8
Open Questions:          2 flagged

Next Step:
  → Feed output into api-spec-to-technical-plan skill:
    "Run api-spec-to-technical-plan on
     customer-management-api-spec-2025-03-16.md"

═══════════════════════════════════════════════════════════════════
```

---

## Adaptation Rules

| Condition | Behavior |
|---|---|
| No project detected | Use standard REST conventions |
| Story implies custom action (not CRUD) | Generate POST /{resource}/{id}/{action} endpoint |
| Multiple actors in one story | Define separate permissions per actor |
| Story mentions search/filter | Add query parameters to GET list endpoint |
| Story mentions file upload | Add multipart/form-data request body |
| Ambiguous field type | Default to string, flag as open question |
| No benefit stated in story | Infer from feature description, flag as assumption |

