Add API versioning policy and sync docs OpenAPI spec
This commit is contained in:
parent
f7062f2bdc
commit
7bab2b3fb7
3 changed files with 748 additions and 4 deletions
57
docs/api-versioning.md
Normal file
57
docs/api-versioning.md
Normal 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.
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue