Version: 1.0
Backend Phase: 01-destination-module (Complete)
Last Updated: 2026-04-30
This document maps the Balsamiq wireframes to actual API endpoints, showing where, when, and how each API is used in the admin panel.
| Wireframe Screen | Tab / Action | API Endpoint | Method |
|---|---|---|---|
| Destination List | Load list | GET /destinations |
Public |
| Destination List | Filter by type | GET /destinations?type=country |
Public |
| Destination List | Toggle status | PATCH /destinations/:id/status |
Auth |
| Destination List | Soft delete | DELETE /destinations/:id |
Auth |
| Add Destination | Load parent dropdown | GET /destinations/parent-options?type=country |
Public |
| Add Destination | Save (Hero & Intro) | POST /destinations |
Auth |
| Edit Destination | Load full data | GET /destinations/:id |
Public |
| Edit Destination | Save Hero & Intro | PATCH /destinations/:id |
Auth |
| Edit Destination | Save Overview Facts | PUT /destinations/:id/overview |
Auth |
| Edit Destination | Save Highlights | PUT /destinations/:id/sections/highlights |
Auth |
| Edit Destination | Upload image in Highlights | POST /destinations/:id/sections/highlights/media |
Auth |
| Edit Destination | Save Climate | PUT /destinations/:id/sections/climate |
Auth |
| Edit Destination | Save Getting There | PUT /destinations/:id/sections/getting_there |
Auth |
| Edit Destination | Save Docs & Health | PUT /destinations/:id/sections/docs_health |
Auth |
When: On initial page load or when filters change.
API:
GET /destinations?type=&parentId=&status=
Query Parameters (all optional):
| Param | Type | Description |
|---|---|---|
type |
enum | world_area, country, region |
parentId |
number | Filter by parent destination ID |
status |
enum | published, unpublished |
Response:
{
"message": "SUC_DESTINATION_LIST",
"data": {
"destinations": [
{
"id": 1,
"name": "Europe",
"type": "world_area",
"status": "published",
"updatedAt": "2026-04-30T10:00:00.000Z"
},
{
"id": 2,
"name": "France",
"type": "country",
"status": "published",
"updatedAt": "2026-04-30T09:30:00.000Z"
}
],
"totalCount": 2
}
}
Frontend Mapping:
data.destinationsstatusWhen: User clicks the status toggle button on a list row.
API:
PATCH /destinations/:id/status
Authorization: Bearer <jwt_token>
Response:
{
"message": "SUC_DESTINATION_STATUS_UPDATED",
"data": {
"id": 2,
"status": "unpublished"
}
}
Frontend Action:
When: User clicks the Delete button (with confirmation dialog).
API:
DELETE /destinations/:id
Authorization: Bearer <jwt_token>
Response:
{
"message": "SUC_DESTINATION_DELETED",
"data": {}
}
Frontend Action:
This is the mandatory first tab when creating or editing a destination.
When: User selects type = country or region in the form. The parent dropdown must populate dynamically.
API:
GET /destinations/parent-options?type=country
Query Parameters:
| Param | Value | Returns |
|---|---|---|
type=country |
— | All world_area destinations |
type=region |
— | All country destinations |
Response:
{
"message": "SUC_PARENT_OPTIONS",
"data": [
{ "id": 1, "name": "Europe" },
{ "id": 3, "name": "Asia" }
]
}
Frontend Mapping:
<select> dropdown with id as value, name as labeltype = world_area → parentId must be null (dropdown hidden/disabled)type = country → parentId must reference a world_areatype = region → parentId must reference a countryWhen: User fills the Hero & Intro form and clicks Save for the first time.
API:
POST /destinations
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"name": "France",
"type": "country",
"parentId": 1,
"heroImage": "https://cdn.example.com/france-hero.jpg",
"description": "A beautiful country known for its wine, cuisine, and culture."
}
Field Rules:
| Field | Required | Rules |
|---|---|---|
name |
Yes | Max 100 chars |
type |
Yes | world_area, country, region |
parentId |
Conditional | Required for country/region, null for world_area |
heroImage |
No | Max 500 chars (URL) |
description |
No | Free text |
Response (Success — 201):
{
"message": "SUC_DESTINATION_CREATED",
"data": {
"id": 2,
"name": "France",
"type": "country",
"parentId": 1,
"heroImage": "https://cdn.example.com/france-hero.jpg",
"description": "A beautiful country known for its wine, cuisine, and culture.",
"status": "unpublished",
"createdAt": "2026-04-30T10:00:00.000Z",
"updatedAt": "2026-04-30T10:00:00.000Z"
}
}
Frontend Action:
id (e.g., 2) in local state or URL paramsError Response (400 — Invalid Parent):
{
"statusCode": 400,
"message": "ERR_INVALID_PARENT_COUNTRY"
}
When: User edits an existing destination's base fields and clicks Save.
API:
PATCH /destinations/:id
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body: (same as create, all fields optional)
{
"name": "France Updated",
"heroImage": "https://cdn.example.com/france-hero-v2.jpg",
"description": "Updated description."
}
Response:
{
"message": "SUC_DESTINATION_UPDATED",
"data": {
"id": 2,
"name": "France Updated",
"type": "country",
"parentId": 1,
"heroImage": "https://cdn.example.com/france-hero-v2.jpg",
"description": "Updated description.",
"status": "published",
"updatedAt": "2026-04-30T11:00:00.000Z"
}
}
When: User clicks the Overview Facts tab.
API:
GET /destinations/:id/overview
Response:
{
"message": "SUC_OVERVIEW_DETAILS",
"data": [
{ "id": 1, "key": "Currency", "value": "Euro (EUR)", "order": 1 },
{ "id": 2, "key": "Language", "value": "French", "order": 2 },
{ "id": 3, "key": "Time Zone", "value": "CET (UTC+1)", "order": 3 },
{ "id": 4, "key": "Best Time to Travel", "value": "April to June", "order": 4 }
]
}
Frontend Mapping:
order field controls display sequenceWhen: User clicks Save in the Overview Facts tab. This sends the entire array of facts — the backend replaces all existing facts atomically.
API:
PUT /destinations/:id/overview
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"items": [
{ "key": "Currency", "value": "Euro (EUR)", "order": 1 },
{ "key": "Language", "value": "French", "order": 2 },
{ "key": "Time Zone", "value": "CET (UTC+1)", "order": 3 },
{ "key": "Best Time to Travel", "value": "April to June", "order": 4 }
]
}
Important Notes:
items array replaces all existing facts. If a key is removed from the array, it is deleted from the database.order is optional; if omitted, the backend auto-assigns index + 1.Response:
{
"message": "SUC_OVERVIEW_UPDATED",
"data": {
"destinationId": 2,
"count": 4
}
}
Frontend Action:
When: User clicks the Highlights tab.
API:
GET /destinations/:id/sections/highlights
Response:
{
"message": "SUC_SECTION_DETAILS",
"data": {
"id": 5,
"destinationId": 2,
"sectionType": "highlights",
"content": "<p>France is famous for the Eiffel Tower, Louvre Museum, and Provence lavender fields.</p>"
}
}
If section does not exist yet:
{
"message": "SUC_SECTION_DETAILS",
"data": null
}
Frontend Mapping:
content into a rich-text editor (e.g., Quill, CKEditor, TinyMCE)data is null, show an empty editorWhen: User clicks Save in the Highlights tab.
API:
PUT /destinations/:id/sections/highlights
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"content": "<p>France is famous for the Eiffel Tower, Louvre Museum, and Provence lavender fields.</p>"
}
Response:
{
"message": "SUC_SECTION_UPDATED",
"data": {
"id": 5,
"destinationId": 2,
"sectionType": "highlights"
}
}
When: User clicks the image upload button inside the rich-text editor.
API:
POST /destinations/:id/sections/highlights/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>
Request Body (multipart):
| Field | Type | Description |
|---|---|---|
mediaFile |
File | Image file (JPEG, PNG, WebP). Max 5MB. |
Response:
{
"message": "SUC_SECTION_MEDIA_UPLOADED",
"data": {
"id": 10,
"fileName": "eiffel-tower.jpg",
"link": "https://cdn.example.com/destinations/section-media/eiffel-tower-abc123.jpg"
}
}
Frontend Integration Flow:
<img src="{data.link}" /> into the editor's HTML content at the cursor positioncontent HTMLPUT /sections/highlights call saves the HTML including the <img> tagListing Section Media (Optional — for media gallery):
GET /destinations/:id/sections/highlights/media
Response:
{
"message": "SUC_SECTION_MEDIA_LIST",
"data": [
{
"id": 10,
"fileName": "eiffel-tower.jpg",
"link": "https://cdn.example.com/destinations/section-media/eiffel-tower-abc123.jpg",
"createdAt": "2026-04-30T12:00:00.000Z"
}
]
}
Delete Section Media:
DELETE /destinations/:id/sections/highlights/media/:mediaId
Authorization: Bearer <jwt_token>
Identical pattern to Highlights, with type = climate.
GET /destinations/:id/sections/climate
PUT /destinations/:id/sections/climate
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"content": "<p>France has a temperate climate with mild winters and warm summers. The Mediterranean coast enjoys hot, dry summers.</p>"
}
POST /destinations/:id/sections/climate/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>
Identical pattern, with type = getting_there.
GET /destinations/:id/sections/getting_there
PUT /destinations/:id/sections/getting_there
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"content": "<p>France is well-connected by air, rail, and road. Major airports include Charles de Gaulle (Paris) and Nice Cote d'Azur.</p>"
}
POST /destinations/:id/sections/getting_there/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>
Identical pattern, with type = docs_health.
GET /destinations/:id/sections/docs_health
PUT /destinations/:id/sections/docs_health
Content-Type: application/json
Authorization: Bearer <jwt_token>
Request Body:
{
"content": "<p>Valid passport required. No visa for EU citizens. Vaccination certificate may be required for certain regions.</p>"
}
POST /destinations/:id/sections/docs_health/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>
When opening an existing destination for editing, the frontend can either:
Option A — Lazy Load (Recommended):
Option B — Eager Load:
GET /destinations/:id
Response:
{
"message": "SUC_DESTINATION_FULL",
"data": {
"id": 2,
"name": "France",
"type": "country",
"parentId": 1,
"heroImage": "https://cdn.example.com/france-hero.jpg",
"description": "A beautiful country...",
"status": "published",
"overview": [
{ "id": 1, "key": "Currency", "value": "Euro (EUR)", "order": 1 },
{ "id": 2, "key": "Language", "value": "French", "order": 2 }
],
"sections": {
"highlights": {
"content": "<p>France is famous for...</p>",
"media": [
{ "id": 10, "fileName": "eiffel-tower.jpg", "link": "https://cdn.example.com/..." }
]
},
"climate": null,
"getting_there": null,
"docs_health": null
}
}
}
Note: The sections object keys are highlights, climate, getting_there, docs_health. Values are either { content, media } objects or null if the section has not been created yet.
| Tab | APIs Called On Switch |
|---|---|
| Hero & Intro | Already loaded from GET /destinations/:id or base state |
| Overview Facts | GET /destinations/:id/overview |
| Highlights | GET /destinations/:id/sections/highlights + GET /destinations/:id/sections/highlights/media |
| Climate | GET /destinations/:id/sections/climate + GET /destinations/:id/sections/climate/media |
| Getting There | GET /destinations/:id/sections/getting_there + GET /destinations/:id/sections/getting_there/media |
| Docs & Health | GET /destinations/:id/sections/docs_health + GET /destinations/:id/sections/docs_health/media |
Optimization: Cache section data in component state so revisiting a tab doesn't refetch.
All write endpoints (POST, PUT, PATCH, DELETE) require:
Authorization: Bearer <jwt_access_token>
The user must have Permission.DESTINATION = 18 assigned to their role.
User clicks "Insert Image" in editor
|
v
File picker opens
|
v
User selects image file
|
v
POST /destinations/:id/sections/:type/media
|
v
Backend uploads to S3, returns { link }
|
v
Frontend inserts <img src="{link}" /> into editor HTML
|
v
User clicks "Save" tab
|
v
PUT /destinations/:id/sections/:type
Body: { "content": "<p>...<img src=\"{link}\">...</p>" }
|
v
Section saved with embedded image URLs
| Error Code | HTTP Status | When It Happens | Frontend Action |
|---|---|---|---|
ERR_INVALID_PARENT_WORLD_AREA |
400 | world_area has a parentId |
Show inline error on parent dropdown |
ERR_INVALID_PARENT_COUNTRY |
400 | country parent is not a world_area |
Show inline error on parent dropdown |
ERR_INVALID_PARENT_REGION |
400 | region parent is not a country |
Show inline error on parent dropdown |
ERR_DESTINATION_NAME_EXISTS |
400 | Name already used (case-insensitive) | Show inline error on name field |
ERR_DESTINATION_NOT_FOUND |
404 | ID does not exist or is soft-deleted | Redirect to list page with error toast |
ERR_INVALID_SECTION_TYPE |
400 | Section type is not one of the 4 allowed | Internal bug — should not happen |
ERR_MEDIA_FILE_REQUIRED |
400 | Image upload called without a file | Show "Please select a file" message |
{
"statusCode": 400,
"message": "ERR_INVALID_PARENT_COUNTRY"
}
| Method | Endpoint | Description |
|---|---|---|
| GET | /destinations |
List destinations with filters |
| GET | /destinations/parent-options?type= |
Get parent dropdown options |
| GET | /destinations/:id |
Get full destination with overview and sections |
| GET | /destinations/:id/overview |
Get overview facts |
| GET | /destinations/:id/sections/:type |
Get section by type |
| GET | /destinations/:id/sections/:type/media |
List section media |
| Method | Endpoint | Description |
|---|---|---|
| POST | /destinations |
Create destination |
| PATCH | /destinations/:id |
Update destination base fields |
| PATCH | /destinations/:id/status |
Toggle publish/unpublish |
| DELETE | /destinations/:id |
Soft delete destination |
| PUT | /destinations/:id/overview |
Replace all overview facts |
| PUT | /destinations/:id/sections/:type |
Upsert section content |
| POST | /destinations/:id/sections/:type/media |
Upload section media |
| DELETE | /destinations/:id/sections/:type/media/:mediaId |
Delete section media |
:type can only be one of:
highlightsclimategetting_theredocs_healthEnd of Frontend Integration Guide