Skip to content

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

  1. Überblick
  2. Authentication
  3. API-Endpoints
  4. Error Handling
  5. Rate Limiting
  6. Pagination
  7. OpenAPI Specification

Ü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

http
Content-Type: application/json
Authorization: Bearer <JWT_ACCESS_TOKEN>
Accept: application/json

Authentication

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:

json
{
  "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

json
{
  "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:

json
{
  "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:

json
{
  "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 with 401.
  • 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 }. Emits partner.delete audit 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 yet
  • POST /users/me/avatar (multipart upload) — avatars are set via PUT /users/me (avatarType + avatarId/avatarUrl)
  • POST /tasks/:id/apply — replaced by POST /tasks/:id/accept
  • POST /ratings, GET /users/:id/ratings — trust score is computed server-side; no rating endpoints exist
  • GET /points/leaderboard — not implemented
  • POST /redemptions, GET /users/me/redemptions — redemption is now POST /partners/:id/redeem; per-user redemption history is not exposed yet

Error Handling

Standard Error Response

json
{
  "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 StatusError CodeDescription
400VALIDATION_ERRORRequest validation failed
401UNAUTHORIZEDMissing or invalid authentication
403FORBIDDENInsufficient permissions
404NOT_FOUNDResource not found
409CONFLICTResource already exists
422UNPROCESSABLE_ENTITYBusiness logic error
429RATE_LIMIT_EXCEEDEDToo many requests
500INTERNAL_SERVER_ERRORServer error
503SERVICE_UNAVAILABLEService temporarily unavailable

Common Error Examples

401 Unauthorized

json
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired access token",
    "timestamp": "2026-06-05T10:30:00Z"
  }
}

403 Forbidden

json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "You do not have permission to perform this action",
    "timestamp": "2026-06-05T10:30:00Z"
  }
}

422 Business Logic Error

json
{
  "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

http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1717502400

429 Response

json
{
  "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.

http
GET /users/me/tasks?page=2&limit=20

Two response shapes appear across the API:

jsonc
// 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:

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:

http
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:

  1. Committed openapi.yaml (currently generated at runtime only)
  2. Postman collection generation from the Swagger spec
  3. Auto-generated TypeScript client shared between mobile and admin

Good Deeds - Nachbarschaftshilfe-App