Spec: 02 · Manage Fruits
Taiga Story: #2 · 02-manage-fruits
Status: Ready
Branch: feature/02-manage-fruits
Depends on: 01-Bootstrap
1. Goal
Maintainers can create, read, update, and delete fruits. Each fruit has a name, OSDB Kürzel (unique number), optional comment, synonyms list, a required type, and an images collection.
2. Background / Scope / Context / Constraints
- "OSDB Kürzel" is the public label for the internal
osdb_number field (unique identifier used by legacy system).
- Fruit type is a fixed enum — no free-form entry.
- Images have an image type (
fruit, flower, tree), a filename, and binary data stored in the DB.
- Synonyms are a 1:N child table (not a comma-separated string in the parent row).
- API must support listing, filtering (prepare for story #6), and CRUD.
- No authentication guard yet (added in story #8); all endpoints are public for now.
Fruit Type Enum
3. Necessary Refactorings
None from prior stories (only bootstrap exists). Echo router structure should be established here as GET /api/v1/fruits, POST /api/v1/fruits, etc.
4. Related Data Model
5. Affected Places / Related Components
Backend
| File |
Purpose |
backend/internal/domain/fruit.go |
Fruit, FruitSynonym, FruitImage structs |
backend/internal/repository/fruit_repo.go |
DB queries (list, get, create, update, delete, images) |
backend/internal/handler/fruit_handler.go |
Echo handlers — request bind/validate, call repo, return JSON |
backend/cmd/server/router.go |
Register /api/v1/fruits route group |
migrations/000002_create_fruits.up.sql |
Schema as above |
migrations/000002_create_fruits.down.sql |
DROP TABLE fruit_images, fruit_synonyms, fruits; DROP TYPE fruit_type |
Frontend
| File |
Purpose |
frontend/src/api/fruits.ts |
Axios/fetch wrapper — listFruits, getFruit, createFruit, updateFruit, deleteFruit, addImage, deleteImage |
frontend/src/stores/fruitStore.ts |
Pinia store |
frontend/src/views/FruitList.vue |
Table/card list, search bar (placeholder for #6), link to detail |
frontend/src/views/FruitDetail.vue |
Show + edit form, synonyms editor, image gallery with upload/delete |
frontend/src/views/FruitCreate.vue |
Create form |
frontend/src/router/index.ts |
Add routes: /fruits, /fruits/:id, /fruits/new |
API Endpoints
| Method |
Path |
Description |
| GET |
/api/v1/fruits |
List (query params: name, type) |
| POST |
/api/v1/fruits |
Create |
| GET |
/api/v1/fruits/:id |
Get by ID (includes synonyms) |
| PUT |
/api/v1/fruits/:id |
Full update |
| DELETE |
/api/v1/fruits/:id |
Delete (cascades images+synonyms) |
| GET |
/api/v1/fruits/:id/images |
List images (metadata only, no binary) |
| POST |
/api/v1/fruits/:id/images |
Upload image (multipart) |
| GET |
/api/v1/fruits/:id/images/:imageId |
Serve binary image |
| DELETE |
/api/v1/fruits/:id/images/:imageId |
Delete image |
6. Out of Scope
- Search/filter implementation (story #6)
- Authentication guards (story #8)
- Thumbnail generation (story #7)
- Bulk import (story #3)
- Publications link (story #4)
7. Verification
| Check |
Expected |
POST /api/v1/fruits with missing name |
422 Unprocessable Entity |
POST /api/v1/fruits with duplicate osdb_number |
409 Conflict |
GET /api/v1/fruits/:id |
Returns fruit with synonyms array |
POST /api/v1/fruits/:id/images (multipart JPEG) |
201 Created; GET image serves correct bytes |
DELETE /api/v1/fruits/:id |
204; cascades synonyms+images |
| Frontend: create fruit → appears in list |
No page reload needed (Pinia reactive) |
| Frontend: upload image → thumbnail shown |
Image served from backend |
go test ./... |
All handler + repo tests green |
npm run test |
Vitest tests for store and API wrapper green |
8. Open Questions
| # |
Question |
Impact |
| 1 |
Image upload max size limit? |
Multipart config, DB row size |
| 2 |
Should GET /api/v1/fruits return image URLs or inline base64 thumbnails? |
Network perf vs simplicity |
| 3 |
Pagination for fruit list (offset/limit) or full list? |
UX + performance with large dataset |
| 4 |
PUT vs PATCH for updates? |
Client complexity |