Add API versioning policy and sync docs OpenAPI spec

This commit is contained in:
openclaw 2026-03-05 15:13:48 +00:00
parent f7062f2bdc
commit 7bab2b3fb7
3 changed files with 748 additions and 4 deletions

57
docs/api-versioning.md Normal file
View file

@ -0,0 +1,57 @@
# API Versionierung und Deprecation Policy
## Ziel
Diese Policy definiert eine reproduzierbare Versionierungsstrategie fuer `helpyourneighbour`, damit API-Clients stabil bleiben und Breaking Changes planbar sind.
## Versionierungsmodell
- Basispfad-Versionierung: `/v1/...`, `/v2/...`
- Aktueller Stand dieses Repos ist funktional `v1`.
- Fuer die laufende Migration gilt: bestehende Routen ohne Prefix bleiben bis zur vollstaendigen Umstellung als Legacy-Compat aktiv.
## Semantik
- **Minor Change (non-breaking):** neue optionale Felder, neue Endpunkte, neue optionale Query-Parameter.
- **Major Change (breaking):** Umbenennung/Entfernung von Feldern, strictere Validierung mit API-Break, geaenderte Fehlersemantik.
- Non-breaking Aenderungen bleiben innerhalb derselben Major API (`v1`).
- Breaking Aenderungen erzeugen eine neue Major API (`v2`).
## Deprecation-Regeln
Bei geplanter Entfernung eines Endpunkts/Felds:
1. Markierung als `deprecated: true` in OpenAPI.
2. Deprecation-Hinweis in Response-Headern:
- `Deprecation: true`
- `Sunset: <RFC-1123 Datum>`
- optional `Link: <migrations-url>; rel=\"deprecation\"`
3. Changelog-Eintrag und Migrationshinweise im Repo (`docs/`).
4. Mindestfrist bis Removal: **90 Tage**.
## Kompatibilitaetsgarantien
- JSON-Felder werden nicht ohne neue Major API entfernt.
- Typen werden nicht stillschweigend geaendert.
- Neue Pflichtfelder werden nicht in bestehende Request-Bodies von `v1` eingefuehrt.
## Fehlerverhalten
- Fehlerobjekte folgen stabil dem Muster `{ "error": ... }`.
- Neue Fehlercodes werden additive eingefuehrt.
- Bereits genutzte HTTP-Statuscodes bleiben stabil, ausser bei neuer Major API.
## Rollout-Prozess fuer Breaking Changes
1. ADR erstellen (Design + Impact).
2. `v2` Endpunkt parallel einfuehren.
3. `v1` als deprecated markieren (Header + OpenAPI).
4. 90-Tage-Frist einhalten.
5. Nach Frist `v1` entfernen.
## Definition of Done fuer API-Aenderungen
- OpenAPI aktualisiert (`/openapi.yaml` und `docs/openapi.yaml`).
- Changelog/Migrationshinweis vorhanden.
- Contract-Tests fuer geaenderte Endpunkte aktualisiert.
- Breaking vs non-breaking explizit im PR beschrieben.

View file

@ -1,5 +1,686 @@
openapi: 3.0.0
openapi: 3.1.0
info:
title: Helpyourneighbour API
version: 0.1.0
paths: {}
title: HelpYourNeighbour API
version: 0.2.0
description: API contract for authentication, requests, offers, contacts, profile, addresses, and reviews.
servers:
- url: http://localhost:3000
description: Local development
security:
- bearerAuth: []
paths:
/health:
get:
tags: [system]
summary: Health check
security: []
responses:
'200':
description: Service healthy
content:
application/json:
schema:
$ref: '#/components/schemas/HealthResponse'
/auth/register:
post:
tags: [auth]
summary: Register a new user
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenResponse'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Email already exists
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Registration failed
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/auth/login:
post:
tags: [auth]
summary: Login with email/password
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Login success
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
description: Invalid credentials
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/requests:
get:
tags: [requests]
summary: List help requests
security: []
responses:
'200':
description: Request list
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/HelpRequest'
post:
tags: [requests]
summary: Create help request
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateHelpRequest'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/offers/{requestId}:
post:
tags: [offers]
summary: Create offer for help request
parameters:
- $ref: '#/components/parameters/RequestId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOfferRequest'
responses:
'201':
description: Offer created
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequestSimple'
'401':
$ref: '#/components/responses/Unauthorized'
/offers/negotiation/{offerId}:
post:
tags: [offers]
summary: Create counter-offer/negotiation message
parameters:
- $ref: '#/components/parameters/OfferId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOfferRequest'
responses:
'201':
description: Negotiation entry created
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequestSimple'
'401':
$ref: '#/components/responses/Unauthorized'
/offers/accept/{offerId}:
post:
tags: [offers]
summary: Accept offer and create deal
parameters:
- $ref: '#/components/parameters/OfferId'
responses:
'201':
description: Deal created
content:
application/json:
schema:
$ref: '#/components/schemas/DealCreatedResponse'
'400':
description: Invalid offer id
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: Offer not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/contacts/request:
post:
tags: [contacts]
summary: Request contact exchange for a deal
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ContactRequestCreate'
responses:
'201':
description: Request created
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Deal not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: Request already exists
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/contacts/respond:
post:
tags: [contacts]
summary: Accept or reject contact exchange request
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ContactRequestRespond'
responses:
'200':
description: Response processed
content:
application/json:
schema:
$ref: '#/components/schemas/StatusResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Request not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/contacts/deal/{dealId}:
get:
tags: [contacts]
summary: List accepted contact exchanges for deal
parameters:
- $ref: '#/components/parameters/DealId'
responses:
'200':
description: Accepted contact exchange rows
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ContactExchangeRow'
'400':
description: Invalid dealId
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Deal not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/addresses/change-request:
post:
tags: [addresses]
summary: Request address change with postal verification code
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddressChangeRequest'
responses:
'201':
description: Change request created
content:
application/json:
schema:
$ref: '#/components/schemas/AddressChangeCreatedResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/addresses/verify:
post:
tags: [addresses]
summary: Verify address change with 6-digit code
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddressVerifyRequest'
responses:
'200':
description: Address verified
content:
application/json:
schema:
$ref: '#/components/schemas/StatusResponse'
'400':
description: Invalid payload or code
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Request not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: Request not pending
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/profile/phone:
post:
tags: [profile]
summary: Update encrypted phone number
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PhoneUpdateRequest'
responses:
'200':
description: Phone updated
content:
application/json:
schema:
$ref: '#/components/schemas/StatusResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/reviews/{dealId}:
post:
tags: [reviews]
summary: Create review for deal
parameters:
- $ref: '#/components/parameters/DealId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateReviewRequest'
responses:
'201':
description: Review created
content:
application/json:
schema:
$ref: '#/components/schemas/IdResponse'
'400':
$ref: '#/components/responses/BadRequestSimple'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
RequestId:
name: requestId
in: path
required: true
schema:
type: integer
minimum: 1
OfferId:
name: offerId
in: path
required: true
schema:
type: integer
minimum: 1
DealId:
name: dealId
in: path
required: true
schema:
type: integer
minimum: 1
responses:
BadRequest:
description: Validation failed
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequestSimple:
description: Invalid payload
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Missing or invalid token
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
HealthResponse:
type: object
additionalProperties: false
properties:
status:
type: string
example: ok
required: [status]
ErrorResponse:
type: object
properties:
error:
oneOf:
- type: string
- type: object
StatusResponse:
type: object
additionalProperties: false
properties:
status:
type: string
required: [status]
IdResponse:
type: object
additionalProperties: false
properties:
id:
type: integer
minimum: 1
required: [id]
DealCreatedResponse:
type: object
additionalProperties: false
properties:
dealId:
type: integer
minimum: 1
required: [dealId]
AuthTokenResponse:
type: object
additionalProperties: false
properties:
token:
type: string
required: [token]
RegisterRequest:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
displayName:
type: string
minLength: 2
maxLength: 120
required: [email, password, displayName]
LoginRequest:
type: object
additionalProperties: false
properties:
email:
type: string
format: email
password:
type: string
minLength: 1
required: [email, password]
HelpRequest:
type: object
properties:
id:
type: integer
title:
type: string
description:
type: string
value_chf:
type: number
status:
type: string
created_at:
type: string
format: date-time
requester_name:
type: string
CreateHelpRequest:
type: object
additionalProperties: false
properties:
title:
type: string
minLength: 3
maxLength: 180
description:
type: string
minLength: 5
valueChf:
type: number
exclusiveMinimum: 0
required: [title, description, valueChf]
CreateOfferRequest:
type: object
additionalProperties: false
properties:
amountChf:
type: number
exclusiveMinimum: 0
message:
type: string
maxLength: 2000
required: [amountChf]
ContactRequestCreate:
type: object
additionalProperties: false
properties:
dealId:
type: integer
minimum: 1
targetUserId:
type: integer
minimum: 1
required: [dealId, targetUserId]
ContactRequestRespond:
type: object
additionalProperties: false
properties:
requestId:
type: integer
minimum: 1
accept:
type: boolean
required: [requestId, accept]
ContactExchangeRow:
type: object
properties:
id:
type: integer
requester_id:
type: integer
target_id:
type: integer
accepted:
type: boolean
requester_phone_encrypted:
type: string
target_phone_encrypted:
type: string
AddressChangeRequest:
type: object
additionalProperties: false
properties:
newAddress:
type: string
minLength: 10
required: [newAddress]
AddressChangeCreatedResponse:
type: object
additionalProperties: false
properties:
requestId:
type: integer
minimum: 1
postalDispatch:
type: string
example: pending_letter
note:
type: string
verificationCode:
type: string
pattern: '^\\d{6}$'
required: [requestId, postalDispatch, note, verificationCode]
AddressVerifyRequest:
type: object
additionalProperties: false
properties:
requestId:
type: integer
minimum: 1
code:
type: string
pattern: '^\\d{6}$'
required: [requestId, code]
PhoneUpdateRequest:
type: object
additionalProperties: false
properties:
phone:
type: string
minLength: 6
maxLength: 40
required: [phone]
CreateReviewRequest:
type: object
additionalProperties: false
properties:
revieweeId:
type: integer
minimum: 1
rating:
type: integer
minimum: 1
maximum: 5
comment:
type: string
maxLength: 2000
required: [revieweeId, rating]