API-Design - Good Deeds Nachbarschaftshilfe-App
Version: 1.1
Datum: 2026-07-08
Autor: CTO
Status: Current (rewritten to match apps/backend/src/)
Base URL: https://api.gooddeeds.app/api/v1 (dev: http://localhost:3000/api/v1)
Protocol: REST/JSON over HTTPS
For a per-feature view (which mobile screen calls which endpoint), see features.md.
Inhaltsverzeichnis
Überblick
Die Good Deeds API ist eine RESTful JSON API für die Nachbarschaftshilfe-App. Sie nutzt JWT für Authentication und folgt REST-Best-Practices.
Design-Prinzipien
- REST-konform: HTTP-Verben semantisch (GET, POST, PATCH, PUT, DELETE)
- JSON-First: Alle Requests/Responses in JSON
- Versioniert: URL-basiertes Versioning (
/api/v1) - Filterbar: GET-Endpoints unterstützen Query-Parameter
- Paginiert: Collections nutzen page/limit Pagination
Common Headers
Content-Type: application/json
Authorization: Bearer <JWT_ACCESS_TOKEN>
Accept: application/jsonAuthentication
JWT-based Authentication
Die API nutzt JWT (JSON Web Tokens) mit Access + Refresh Tokens.
Access Token: 15 Minuten Lifetime
Refresh Token: 7 Tage Lifetime
Auth Flow
1. User → POST /auth/register
2. API → { accessToken, refreshToken, user }
3. User → GET /tasks (Header: Authorization: Bearer <accessToken>)
4. API → 401 (wenn expired)
5. User → POST /auth/refresh { refreshToken }
6. API → { accessToken, refreshToken }Admin routes (/admin/**) use the same JWT scheme via POST /admin/auth/login and enforce the admin role through AdminGuard.
API-Endpoints
Every entry below states method · path · auth. "Auth: user" means a valid JWT bearer token is required; "Auth: admin" means the JWT must belong to an admin user; "Auth: none" means the endpoint is @Public().
1. Authentication
1.1 Register
- POST
/auth/register· Auth: none · Throttle: 3/min
Request:
{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "Max Mustermann"
}Response 201 Created: { accessToken, refreshToken, user }. user has the same shape as GET /auth/me.
1.2 Login
- POST
/auth/login· Auth: none · Throttle: 5/min
Request: { "email": "…", "password": "…" }
Response 200 OK: { accessToken, refreshToken, user }
1.3 Refresh token
- POST
/auth/refresh· Auth: none
Request: { "refreshToken": "…" }
Response 200 OK: { accessToken, refreshToken }
1.4 Logout
- POST
/auth/logout· Auth: user
Request: {}
Response 200 OK: { message: string } (server invalidates refresh tokens for the current user)
1.5 Verify email
- POST
/auth/verify-email· Auth: none
Request: { "token": "…" } (token from verification email)
Response 200 OK: { message: string }
1.6 Resend verification email
- POST
/auth/resend-verification· Auth: none · Throttle: 3 / 5 min
Request: { "email": "user@example.com" }
Response 200 OK: { message: string }
1.7 Forgot password
- POST
/auth/forgot-password· Auth: none · Throttle: 3 / 5 min
Request: { "email": "user@example.com" }
Response 200 OK: { message: string } (always 200 to avoid user enumeration)
1.8 Reset password
- POST
/auth/reset-password· Auth: none
Request: { "token": "…", "password": "NewSecurePass123!" }
Response 200 OK: { message: string }
2. Users
2.5 Get current user
- GET
/auth/me· Auth: user
Response 200 OK: full User entity (id, email, name, username, bio, avatar fields, pointsBalance, trustScore, verified, role, createdAt, …).
2.6 Update own profile
- PUT
/users/me· Auth: user
Request: any subset of
{
"username": "helpful_neighbor",
"name": "Max Mustermann",
"bio": "Passionate about helping my community!",
"avatarType": "predefined", // or "photo"
"avatarId": "badger-1", // required when avatarType = "predefined"
"avatarUrl": "https://…" // required when avatarType = "photo"
}Response 200 OK: updated User entity.
2.7 Get public profile
- GET
/users/:id· Auth: user
Response 200 OK (PublicProfileDto): { id, username?, name, bio?, avatarUrl?, avatarType?, avatarId?, pointsBalance, trustScore, verified, completedDeeds, tier, createdAt }.
2.8 List predefined avatars
- GET
/users/avatars· Auth: none
Response 200 OK: AvatarDto[] where each item is { id, name, previewUrl, category }.
2.9 My posted tasks
- GET
/users/me/tasks?page=1&limit=20· Auth: user
Response 200 OK: { tasks: Task[], total, page, totalPages }.
2.10 Tasks I helped with
- GET
/users/me/helped?page=1&limit=20· Auth: user
Response 200 OK: { tasks: Task[], total, page, totalPages }.
3. Tasks
Task status enum: open | accepted | pending_confirmation | done | cancelled.
3.1 List nearby tasks
- GET
/tasks?longitude={lng}&latitude={lat}&radiusKm={km}· Auth: user
Query params: longitude (-180 to 180), latitude (-90 to 90), radiusKm (0.1 to 100, default 5). Returns [] if longitude or latitude missing.
Response 200 OK: Task[].
3.2 Create task
- POST
/tasks· Auth: user
Request:
{
"title": "Help needed with moving furniture",
"description": "…",
"categoryId": "household", // optional string
"longitude": 13.405,
"latitude": 52.52,
"imageUrl": "https://…", // optional
"estimatedHours": 2.5, // optional, 0.5–24, must be a 0.5 increment
"requiresVerifiedHelper": false // optional, default false
}Response 201 Created: Task.
3.3 Get task by id
- GET
/tasks/:id· Auth: user
Response 200 OK: Task.
3.4 Update task (creator only)
- PUT
/tasks/:id· Auth: user
Request: any subset of { title, description, categoryId, longitude, latitude }.
Response 200 OK: Task. 403 if caller is not creator.
3.5 Delete task (creator only, must be open)
- DELETE
/tasks/:id· Auth: user
Response 204 No Content. 403 if not creator, 400 if task is not open.
3.6 Accept task (helper)
- POST
/tasks/:id/accept· Auth: user (no body)
Marks the caller as helper. Response 200 OK: Task. 400 if task not open.
3.7 Mark task complete (helper)
- POST
/tasks/:id/complete· Auth: user (no body)
Response 200 OK: Task with status pending_confirmation. 403 if caller is not helper.
3.8 Confirm completion (creator)
- PUT
/tasks/:id/confirm· Auth: user (no body)
Response 200 OK: Task with status done; helper is credited with points. 403 if caller is not creator, 400 if task not in pending_confirmation.
4. Interactions (messaging + time proposals)
Every task with an accepted helper spawns a TaskInteraction entity that groups messages and time-proposal negotiation. All endpoints require the caller to be the task creator or the helper.
Interaction status: active | archived | resolved.
Time-proposal status: pending | accepted | rejected | countered | expired.
4.1 Send message
- POST
/tasks/:taskId/messages· Auth: user (creator/helper)
Request: { "content": "When would you like me to come?", "taskInteractionId": "<uuid>" } (1–2000 chars).
Response 201 Created: Message ({ id, content, senderId, sender, isRead, createdAt }).
4.2 List messages
- GET
/tasks/:taskId/messages?page=1&limit=50· Auth: user (creator/helper)
Response 200 OK: { data: Message[], pagination: { page, limit, total, pages } }.
4.3 Mark message as read
- PATCH
/tasks/:taskId/messages/:messageId/read· Auth: user (recipient only)
Response 200 OK: Message. 403 when trying to mark own message as read.
4.4 Create time proposal
- POST
/tasks/:taskId/time-proposals· Auth: user (creator/helper)
Request:
{
"taskInteractionId": "<uuid>",
"proposedDateTime": "2026-07-01T14:00:00Z", // ISO8601, must be ≥24h in the future
"proposerTimezone": "Europe/Berlin", // optional IANA
"notes": "…" // optional, ≤500 chars
}Response 201 Created: TimeProposal.
4.5 List time proposals
- GET
/tasks/:taskId/time-proposals?page=1&limit=50· Auth: user (creator/helper)
Response 200 OK: { data: TimeProposal[], pagination: {…} }.
4.6 Accept proposal
- POST
/tasks/:taskId/time-proposals/:proposalId/accept· Auth: user (non-proposer party)
Response 200 OK: TimeProposal with status accepted. 403 if caller is the proposer, 400 if not pending/countered.
4.7 Reject proposal
- POST
/tasks/:taskId/time-proposals/:proposalId/reject· Auth: user (non-proposer party)
Request: { "reason": "…" } (optional, ≤200 chars).
Response 200 OK: TimeProposal with status rejected.
4.8 Counter proposal
- POST
/tasks/:taskId/time-proposals/:proposalId/counter· Auth: user (non-proposer party)
Request: same shape as 4.4 minus taskInteractionId (server infers).
Response 201 Created: new TimeProposal linked to the countered one.
4.9 Task interaction thread
- GET
/tasks/:taskId/interactions?page=1&limit=50· Auth: user (creator/helper)
Response 200 OK: { data: InteractionWithDetails[], pagination: {…} }. Each item embeds messages[] and timeProposals[].
4.10 User inbox
- GET
/users/:userId/interactions?page=1&limit=50· Auth: user (must equal:userId)
Response 200 OK: { data: InteractionInboxItem[], pagination: {…} }. Each item adds unreadCount and a lastMessage preview (content truncated to 100 chars, senderId, createdAt) or null when the thread has no messages.
5. Points
5.1 Get balance
- GET
/users/:userId/points· Auth: user
Response 200 OK: { userId, balance }.
5.2 Transaction history
- GET
/users/:userId/points/history?page=1&limit=20· Auth: user
limit is capped at 100. Transaction type enum: task_helper, task_creator_bonus, redemption, admin_adjustment, bonus.
Response 200 OK: { transactions: PointTransaction[], total, page, limit, totalPages } where a PointTransaction is { id, amount, type, referenceId | null, createdAt } (positive amount = earned, negative = spent).
6. Partners & Redemptions
6.1 List partners
- GET
/partners· Auth: user
Response 200 OK: PartnerDto[] ({ id, name, description, logoUrl, category, isActive, createdAt }).
6.2 Get partner with offers
- GET
/partners/:id· Auth: user
Response 200 OK: PartnerWithOffers = PartnerDto + { offers: OfferDto[] }.OfferDto: { id, partnerId, title, description, pointsRequired, discountDescription, isActive, createdAt }.
6.3 Redeem an offer
- POST
/partners/:id/redeem· Auth: user
Request: { "offerId": "<uuid>" }.
Response 200 OK: { code, expiresAt, offerTitle, partnerName }. Fails 422 when balance is insufficient.
7. Admin
All admin endpoints require Auth: admin (JWT for a user whose role is admin). See Spec 003 for the reason suspend and unsuspend share one endpoint.
7.1 Admin login / refresh
- POST
/admin/auth/login· Auth: none · Throttle: 5/min — same body/response as/auth/login, but rejects non-admin accounts with401. - POST
/admin/auth/refresh· Auth: none · Throttle: 10/min — same as/auth/refresh.
7.2 Dashboard stats
- GET
/admin/stats· Auth: admin
Response 200 OK: { totalUsers, activeUsers, verifiedUsers, suspendedUsers, newUsersThisMonth, totalPointsInCirculation, … } (all counters from UsersService.getUserStats + PointsService.getTotalPointsInCirculation).
7.3 List users
- GET
/admin/users?page&limit&status&verified&search· Auth: admin
status: active | inactive | banned. verified: all | verified | unverified. search matches email/name/username.
Response 200 OK: paginated user list.
7.4 Verify / unverify user
- PATCH
/admin/users/:id/verify· Auth: admin
Request: { "verified": true, "reason": "…" } (reason optional, ≤500 chars).
Response 200 OK: { success: true, user }. Emits an audit log with action user.verify.
7.5 Suspend / unsuspend user
- PATCH
/admin/users/:id/suspend· Auth: admin
Request: { "suspended": true|false, "reason": "…" } (reason ≥10 chars when provided).
Response 200 OK: { success: true, user }. Emits an audit log with action user.suspend or user.unsuspend.
7.6 User activities
- GET
/admin/users/:id/activities· Auth: admin
Response 200 OK: { activities: Activity[], total }.
7.7 Verification queue
- GET
/admin/verifications· Auth: admin — returns users pending verification. - POST
/admin/verifications/:id/approve· Auth: admin — no body; response{ success: true, user }. - POST
/admin/verifications/:id/reject· Auth: admin — body{ "reason": "…" }; response{ success: true, user }.
7.8 Points admin
- POST
/admin/points/adjust· Auth: admin
Request: { "userId": "<uuid>", "amount": 100, "reason": "…" } (reason 10–500 chars, amount signed integer).
Response 200 OK: { success: true, transaction }. Emits an audit log with action points.adjust.
- GET
/admin/points/transactions?userId&type&page&limit· Auth: admin — paginated ledger view.
7.9 Partner admin
- GET
/admin/partners· Auth: admin — full partner list including inactive. - POST
/admin/partners· Auth: admin — body{ name, description?, logoUrl?, category?, isActive? }. - PUT
/admin/partners/:id· Auth: admin — same shape as create, all fields optional. - DELETE
/admin/partners/:id· Auth: admin — soft-delete, response{ success: true, partner }. Emitspartner.deleteaudit log. - POST
/admin/partners/:id/offers· Auth: admin — create offer under a partner. - PUT
/admin/partners/:id/offers/:offerId· Auth: admin — update offer.
7.10 Audit logs
- GET
/admin/audit-logs?adminId&action&targetType&from&to&page&limit· Auth: admin
action enum: user.verify, user.suspend, user.unsuspend, user.update, points.adjust, partner.create, partner.update, partner.delete. from/to are ISO date strings.
Response 200 OK: { data: AuditLog[], total, page, limit } where each AuditLog includes { id, adminId, admin, action, targetType, targetId, changes, reason, ipAddress, userAgent, createdAt }.
8. Health
- GET
/health· Auth: none — liveness probe used by Railway.
Deprecated / removed
The following sections in earlier revisions of this document described endpoints that were never implemented and have been dropped:
POST /auth/oauth/google— no OAuth flow yetPOST /users/me/avatar(multipart upload) — avatars are set viaPUT /users/me(avatarType+avatarId/avatarUrl)POST /tasks/:id/apply— replaced byPOST /tasks/:id/acceptPOST /ratings,GET /users/:id/ratings— trust score is computed server-side; no rating endpoints existGET /points/leaderboard— not implementedPOST /redemptions,GET /users/me/redemptions— redemption is nowPOST /partners/:id/redeem; per-user redemption history is not exposed yet
Error Handling
Standard Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "Email must be a valid email address"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
],
"timestamp": "2026-06-05T10:30:00Z",
"request_id": "req-uuid-123"
}
}Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Request validation failed |
| 401 | UNAUTHORIZED | Missing or invalid authentication |
| 403 | FORBIDDEN | Insufficient permissions |
| 404 | NOT_FOUND | Resource not found |
| 409 | CONFLICT | Resource already exists |
| 422 | UNPROCESSABLE_ENTITY | Business logic error |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests |
| 500 | INTERNAL_SERVER_ERROR | Server error |
| 503 | SERVICE_UNAVAILABLE | Service temporarily unavailable |
Common Error Examples
401 Unauthorized
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired access token",
"timestamp": "2026-06-05T10:30:00Z"
}
}403 Forbidden
{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action",
"timestamp": "2026-06-05T10:30:00Z"
}
}422 Business Logic Error
{
"error": {
"code": "INSUFFICIENT_POINTS",
"message": "Insufficient points balance for this redemption",
"details": {
"required": 100,
"available": 50
},
"timestamp": "2026-06-05T10:30:00Z"
}
}Rate Limiting
Rate limits are enforced by @nestjs/throttler. Explicit @Throttle decorators exist on POST /auth/register (3/min), POST /auth/login (5/min), POST /admin/auth/login (5/min), POST /admin/auth/refresh (10/min), POST /auth/resend-verification (3 / 5 min), and POST /auth/forgot-password (3 / 5 min). All other endpoints fall under the module-wide default configured in AppModule.
Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1717502400429 Response
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please try again later.",
"retry_after": 60,
"timestamp": "2026-06-05T10:30:00Z"
}
}Pagination
Paginated collections use page/limit query params (1-indexed). limit is typically capped at 100 depending on the endpoint.
GET /users/me/tasks?page=2&limit=20Two response shapes appear across the API:
// Points, users lists
{ "transactions": [...], "total": 42, "page": 1, "limit": 20, "totalPages": 3 }
// Interactions, messages, time proposals
{ "data": [...], "pagination": { "page": 1, "limit": 50, "total": 5, "pages": 1 } }OpenAPI Specification
The backend serves a live Swagger UI:
- Development: http://localhost:3000/api/docs
- Production: https://api.gooddeeds.app/docs
There is no committed openapi.yaml — the spec is generated at runtime from Nest decorators (@ApiTags, @ApiOperation, DTO @ApiProperty).
Versioning
- URL-basiert:
/api/v1,/api/v2 - Breaking Changes: Neue Major-Version
- Deprecation: 6 Monate Notice vor Removal
Categories (Task Categories)
Task.categoryId is a free-form string (nullable). No enum is enforced server-side; the mobile app currently uses category ids such as handwerk, garten, technik, transport, betreuung, haushalt, einkauf, sonstiges, but the backend accepts any string.
Webhooks (Future)
Für Partner-Integration sind Webhooks geplant:
POST <partner_webhook_url>
Content-Type: application/json
X-Webhook-Signature: sha256=...
{
"event": "redemption.created",
"data": {
"id": "redemption-uuid-1",
"discount_code": "REWE-A1B2C3D4",
"user_id": "user-uuid"
},
"timestamp": "2026-06-05T14:30:00Z"
}Nächste Schritte:
- Committed
openapi.yaml(currently generated at runtime only) - Postman collection generation from the Swagger spec
- Auto-generated TypeScript client shared between mobile and admin
