Back to index

API Contract Sheet

Two Column
Generates documents
API Contract Sheet

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

api-contract-sheet-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.6 KB
Build an HTML document with "API Contract Sheet". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
API Contract Sheet with explicit source values and reading conditions.

## What you have to supply
- Provide the source values, labels and decision conditions described by the schema.

## The shape of the input
- `title` — string — 結論を伝える見出し
- `subtitle` — string? — 出典・時点・母集団や仮定
- `method` — string — HTTPメソッド
- `path` — string — エンドポイントのパス
- `auth` — string — 認証と必要権限
- `request` — array (name, type, required, description) — リクエストのフィールド
- `response` — array (name, type, description) — 成功応答のフィールド
- `errors` — array (status, meaning) — 失敗応答のコードと意味

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Do not compare values with incompatible units, populations, or assumptions.

## How to build it
Put the input in a JSON file and run:

```bash
node scripts/render.mjs api-contract-sheet <input.json> --theme ink -o out.html
```

The output is one self-contained file and fetches nothing at render time.

## The source
- https://design-vault.pages.dev/designs/api-contract-sheet/source/template.tsx
- https://design-vault.pages.dev/designs/api-contract-sheet/source/schema.ts
- https://design-vault.pages.dev/designs/api-contract-sheet/source/sample-data.json
- https://design-vault.pages.dev/designs/api-contract-sheet/sample.html

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

7.0 KB

/designs/api-contract-sheet/source/template.tsx

Show contents
import type { ReactNode } from "react";
import type { Props } from "./schema";
function Ledger({heads,rows,caption}:{heads:string[];rows:ReactNode[][];caption?:string}) { return <table className="ledger">{caption && <caption>{caption}</caption>}<thead><tr>{heads.map((h,i)=><th key={i} scope="col">{h}</th>)}</tr></thead><tbody>{rows.map((r,i)=><tr key={i}>{r.map((v,j)=><td key={j} data-label={heads[j]}>{v}</td>)}</tr>)}</tbody></table>; }
function Notes({children}:{children:ReactNode}) { return <details><summary>データ・判断条件を確認</summary>{children}</details>; }

const CSS = String.raw`
.asset{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2rem;line-height:1.6;font-size:var(--text-body-sm)}
.asset *{box-sizing:border-box}.asset header{margin-bottom:2rem}.asset .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);letter-spacing:.08em;color:var(--color-secondary);margin-bottom:.75rem}.asset h1{font-size:var(--text-h1);font-weight:700;line-height:1.35;letter-spacing:-.025em;max-width:32em}.asset h2{font-size:var(--text-h3);font-weight:600;margin-bottom:.75rem}.asset h3{font-size:var(--text-body-md);font-weight:600}.asset .subtitle,.asset .muted{color:var(--color-secondary)}.asset .subtitle{margin-top:.75rem}.asset .number{font-variant-numeric:tabular-nums;font-family:var(--font-mono)}.asset .hero-number{font-size:var(--text-display);line-height:1.2;font-weight:700;letter-spacing:-.04em}.asset .callout{border-left:.25rem solid var(--color-tertiary);padding:1rem 1.5rem;background:var(--color-surface-subtle);margin-bottom:1.5rem}.asset .grid2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.asset .grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem}.asset .panel{padding:1.5rem;background:var(--color-surface-subtle);border-radius:var(--radius-md);min-width:0}.asset .rule{border-top:1px solid var(--color-outline);padding-top:1rem;margin-top:1rem}.asset .pill{display:inline-block;padding:.25rem .75rem;border:1px solid var(--color-outline);border-radius:var(--radius-full);font-size:var(--text-label)}.asset .focus{background:var(--color-tertiary-container);color:var(--color-on-tertiary-container)}.asset .dark{background:var(--color-primary);color:var(--color-on-primary)}.asset .dark .muted{color:var(--color-on-primary-container)}.asset .row{display:grid;grid-template-columns:12rem minmax(0,1fr) 6rem;gap:1rem;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .row>*{min-width:0}.asset .track{height:1rem;position:relative;background:var(--color-neutral);border-radius:var(--radius-sm)}.asset .bar{height:100%;background:var(--color-tertiary);border-radius:var(--radius-sm)}.asset .track.tall{height:2rem}.asset .flex{display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap}.asset .stack>*+*{margin-top:1rem}.asset ul{list-style:disc;padding-left:1.25rem}.asset li+li{margin-top:.5rem}.asset .plot{display:block;width:100%;height:auto;overflow:visible}.asset .plot text{font-family:var(--font-sans);font-size:14px;fill:var(--color-secondary)}.asset .plot .ink{fill:var(--color-primary)}.asset .plot .axis{stroke:var(--color-outline);stroke-width:1}.asset .plot .accent{stroke:var(--color-tertiary);fill:none;stroke-width:3}.asset .legend{display:flex;gap:1.5rem;flex-wrap:wrap;font-size:var(--text-label);margin-bottom:1rem}.asset .legend b{display:inline-block;width:.75rem;height:.75rem;margin-right:.5rem}.asset .ledger{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}.asset .ledger caption{text-align:left;font-weight:600;margin:1rem 0}.asset .ledger th{font-weight:600;color:var(--color-secondary);text-align:left}.asset .ledger th,.asset .ledger td{padding:.75rem 1rem;border-bottom:1px solid var(--color-outline);vertical-align:top;overflow-wrap:anywhere}.asset .ledger th:first-child,.asset .ledger td:first-child{padding-left:0}.asset .ledger th:last-child,.asset .ledger td:last-child{padding-right:0}.asset .ledger tbody tr:hover{background:var(--color-surface-subtle)}.asset details{border-top:1px solid var(--color-outline);margin-top:1.5rem;padding-top:1rem}.asset summary{cursor:pointer;color:var(--color-secondary);padding:.5rem 0;min-height:2.75rem}.asset summary:focus-visible{outline:2px solid var(--color-tertiary);outline-offset:.25rem}.asset .small{font-size:var(--text-label)}.asset .mobile-only{display:none}.asset .horizontal-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.asset .board-column{border-top:.25rem solid var(--color-primary);padding-top:1rem;min-width:0}.asset .board-item{padding:1rem 0;border-bottom:1px solid var(--color-outline)}.asset .board-item p{margin-top:.5rem}.asset .statement{font-size:var(--text-h2);line-height:1.5;font-weight:600}.asset .meta-line{margin-top:.75rem;font-size:var(--text-label);color:var(--color-secondary)}.asset pre{white-space:pre-wrap;overflow-wrap:anywhere}.asset footer{margin-top:2rem;font-size:var(--text-label);color:var(--color-secondary)}
@media(max-width:640px){.asset{padding:1.5rem 1rem}.asset h1{font-size:var(--text-h2)}.asset header{margin-bottom:1.5rem}.asset .grid2,.asset .grid3,.asset .horizontal-list{grid-template-columns:1fr}.asset .row{grid-template-columns:minmax(0,1fr) 5rem;gap:.5rem}.asset .row>:first-child{grid-column:1/-1}.asset .desktop-only{display:none!important}.asset .mobile-only{display:block}.asset .panel{padding:1rem}.asset .ledger,.asset .ledger tbody,.asset .ledger tr,.asset .ledger td{display:block}.asset .ledger thead{display:none}.asset .ledger tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .ledger td{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.75rem;border:0;padding:.25rem 0!important}.asset .ledger td:before{content:attr(data-label);color:var(--color-secondary);font-size:var(--text-label)}.asset .hero-number{font-size:var(--text-h1)}}
@media print{.asset{max-width:none;padding:1rem}.asset details>*{display:block}.asset summary{display:none}}
`;

export default function Template(d: Props) {

return <section className="asset asset-api-contract-sheet"><style>{CSS}</style><header><p className="eyebrow">API CONTRACT SHEET</p><h1>{d.title}</h1>{d.subtitle && <p className="subtitle">{d.subtitle}</p>}</header>
<div className="panel dark"><p className="statement"><span className="number">{d.method}</span> <code>{d.path}</code></p><p className="meta-line muted">{d.auth}</p></div><div className="grid2" style={{marginTop:"1.5rem"}}><article><h2>リクエスト</h2><Ledger heads={["項目","型 / 必須","意味"]} rows={d.request.map(f=>[f.name,f.type+(f.required?" / 必須":" / 任意"),f.description])}/></article><article><h2>成功応答</h2><Ledger heads={["項目","型","意味"]} rows={d.response.map(f=>[f.name,f.type,f.description])}/></article></div><Notes><Ledger heads={["コード","意味"]} rows={d.errors.map(e=>[e.status,e.meaning])}/></Notes>
<footer>仕様表示用。APIへの送信や認証操作は行わない。</footer></section>;
}

schema.ts

1.3 KB

/designs/api-contract-sheet/source/schema.ts

Show contents
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const value = z.number().finite();
const positive = value.positive();
const unique = (values: string[]) => new Set(values).size === values.length;

export const schema = z.object({
  title: text.describe("結論を伝える見出し"),
  subtitle: text.optional().describe("出典・時点・母集団や仮定"),
  method:z.enum(["GET","POST","PUT","PATCH","DELETE"]).describe("HTTPメソッド"),path:z.string().regex(/^\/[A-Za-z0-9_/{}/.:-]+$/).describe("エンドポイントのパス"),auth:text.describe("認証と必要権限"),
  request:z.array(z.object({name:short,type:short,required:z.boolean(),description:text}).strict()).min(1).max(8).describe("リクエストのフィールド"),
  response:z.array(z.object({name:short,type:short,description:text}).strict()).min(1).max(8).describe("成功応答のフィールド"),
  errors:z.array(z.object({status:z.number().int().min(400).max(599),meaning:text}).strict()).min(1).max(5).describe("失敗応答のコードと意味"),
}).strict().refine(d=>unique(d.request.map(f=>f.name))&&unique(d.response.map(f=>f.name))&&unique(d.errors.map(e=>String(e.status))),"フィールド名と失敗コードは一意");
export type Props = z.infer<typeof schema>;

meta.yaml

1.5 KB

/designs/api-contract-sheet/source/meta.yaml

Show contents
id: api-contract-sheet
name:
  ja: API契約の1枚仕様
  en: API Contract Sheet
purpose:
  ja: エンドポイント・認証・入力・出力・失敗応答を1枚で確認する。
  en: API Contract Sheet with explicit source values and reading conditions.
status: published
still: ./still.png
category: specification
catalogOrder: 650
jobs:
  - explain-structure
useCases:
  - process
layoutKinds:
  - 2-column
tags:
  - api-contract-sheet
  - specification
  - offline
  - responsive
whenToUse:
  - ja: エンドポイント・認証・入力・出力・失敗応答を1枚で確認する。
    en: Use to communicate api contract sheet.
avoidWhen:
  - ja: 同じ条件・単位で比較できないデータには使わない
    en: Do not compare values with incompatible units, populations, or assumptions.
requiredInputs:
  - ja: 仕様表示用。APIへの送信や認証操作は行わない。
    en: Provide the source values, labels and decision conditions described by the schema.
relatedLayouts:
  - integration-contract-catalog
dataSchema:
  title: string — 結論を伝える見出し
  subtitle: string? — 出典・時点・母集団や仮定
  method: string — HTTPメソッド
  path: string — エンドポイントのパス
  auth: string — 認証と必要権限
  request: array (name, type, required, description) — リクエストのフィールド
  response: array (name, type, description) — 成功応答のフィールド
  errors: array (status, meaning) — 失敗応答のコードと意味
sources: []

sample-data.json

1.1 KB

/designs/api-contract-sheet/source/sample-data.json

Show contents
{
  "title": "申込受付APIの入力と失敗時の扱いを揃える",
  "subtitle": "架空サンプル|レイアウトの使用例",
  "method": "POST",
  "path": "/applications",
  "auth": "Bearerトークン / applications:write 権限",
  "request": [
    {
      "name": "customerId",
      "type": "string",
      "required": true,
      "description": "顧客の識別子"
    },
    {
      "name": "productId",
      "type": "string",
      "required": true,
      "description": "商品コード"
    },
    {
      "name": "requestKey",
      "type": "string",
      "required": true,
      "description": "再送時も同じ値を使用"
    }
  ],
  "response": [
    {
      "name": "applicationId",
      "type": "string",
      "description": "発行された申込ID"
    },
    {
      "name": "status",
      "type": "string",
      "description": "受付状態"
    }
  ],
  "errors": [
    {
      "status": 400,
      "meaning": "入力形式の不備"
    },
    {
      "status": 403,
      "meaning": "必要権限がない"
    },
    {
      "status": 409,
      "meaning": "同じキーで内容が不一致"
    }
  ]
}