Destination Module — Frontend Integration Guide

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.


Table of Contents

  1. Wireframe-to-API Mapping Overview
  2. Screen 1: Destination List
  3. Screen 2: Add/Edit Destination — Hero & Intro Tab
  4. Screen 2: Overview Facts Tab
  5. Screen 2: Highlights Tab
  6. Screen 2: Climate Tab
  7. Screen 2: Getting There Tab
  8. Screen 2: Docs & Health Tab
  9. Common Patterns
  10. Error Handling
  11. i18n Message Reference
  12. Appendix A: Full API Summary

1. Wireframe-to-API Mapping Overview

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

2. Screen 1: Destination List

2.1 Load Destination List (Initial Page Load)

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:


2.2 Toggle Publish/Unpublish Status

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


2.3 Soft Delete a Destination

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:


3. Screen 2: Add/Edit Destination — Hero & Intro Tab

This is the mandatory first tab when creating or editing a destination.

3.1 Load Parent Dropdown (Conditional)

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:


3.2 Create New Destination (Save on Add)

When: User fills the Hero & Intro form and clicks Save for the first time.

API:

POST /destinations
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>

Request Body (multipart):

Field Type Required Description
name text Yes Destination name. Max 100 chars
type text Yes world_area, country, region
parentId text Conditional Required for country/region, omit for world_area
heroImage File No Hero image file (JPEG, PNG, WebP). Max 5MB
description text No Free text description

Example using FormData:

const formData = new FormData();
formData.append("name", "France");
formData.append("type", "country");
formData.append("parentId", "1");
formData.append("heroImage", fileInput.files[0]); // File object
formData.append("description", "A beautiful country known for its wine, cuisine, and culture.");

fetch("/destinations", {
  method: "POST",
  headers: { Authorization: `Bearer ${token}` },
  body: formData,
});

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:

Error Response (400 — Invalid Parent):

{
  "statusCode": 400,
  "message": "ERR_INVALID_PARENT_COUNTRY"
}

3.3 Update Hero & Intro (Save on Edit)

When: User edits an existing destination's base fields and clicks Save.

API:

PATCH /destinations/:id
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>

Request Body (multipart):

Field Type Required Description
name text No Destination name. Max 100 chars
type text No world_area, country, region
parentId text No Parent destination ID
heroImage File No Hero image file (JPEG, PNG, WebP). Max 5MB. Replaces existing image
description text No Free text description

Example using FormData:

const formData = new FormData();
formData.append("name", "France Updated");
formData.append("heroImage", fileInput.files[0]); // New image file
formData.append("description", "Updated description.");

fetch("/destinations/2", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${token}` },
  body: formData,
});

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"
  }
}

4. Screen 2: Overview Facts Tab

4.1 Load Overview Facts

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, "image": null },
    { "id": 2, "key": "Language", "value": "French", "order": 2, "image": null },
    { "id": 3, "key": "Time Zone", "value": "CET (UTC+1)", "order": 3, "image": null },
    { "id": 4, "key": "Best Time to Travel", "value": "April to June", "order": 4, "image": "https://cdn.example.com/icons/calendar.png" }
  ]
}

Frontend Mapping:


4.2 Save Overview Facts (Replace All)

When: 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, "image": "https://cdn.example.com/icons/calendar.png" }
  ]
}

Important Notes:

Response:

{
  "message": "SUC_OVERVIEW_UPDATED",
  "data": {
    "destinationId": 2,
    "count": 4
  }
}

Frontend Action:


4.3 Upload Overview Fact Image

When: User clicks the image upload button next to an overview fact row.

API:

POST /destinations/:id/overview/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_OVERVIEW_MEDIA_UPLOADED",
  "data": {
    "fileName": "calendar-icon.png",
    "link": "https://cdn.example.com/destination-overview-images/calendar-icon-abc123.png"
  }
}

Frontend Integration Flow:

  1. User clicks "Upload Image" next to a fact row
  2. Frontend opens a file picker
  3. User selects an image
  4. Frontend calls the above API
  5. On success, frontend stores data.link in the fact row's image field
  6. When user clicks Save, the PUT /overview call includes the image URL in the items array

5. Screen 2: Highlights Tab

5.1 Load Highlights Section

When: User clicks the Highlights tab.

API:

GET /destinations/:id/sections/highlights

Response:

{
  "message": "SUC_SECTION_DETAILS",
  "data": {
    "content": "<p>France is famous for the Eiffel Tower, Louvre Museum, and Provence lavender fields.</p>",
    "media": [
      { "id": 10, "fileName": "eiffel-tower.jpg", "link": "https://cdn.example.com/..." }
    ]
  }
}

If section does not exist yet:

{
  "message": "SUC_SECTION_DETAILS",
  "data": null
}

Frontend Mapping:


5.2 Save Highlights Content

When: 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"
  }
}

5.3 Upload Image in Highlights Editor

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:

  1. User clicks "Insert Image" in the editor
  2. Frontend opens a file picker
  3. User selects an image
  4. Frontend calls the above API
  5. On success, frontend inserts <img src="{data.link}" /> into the editor's HTML content at the cursor position
  6. The image URL is now part of the content HTML
  7. When user clicks Save, the PUT /sections/highlights call saves the HTML including the <img> tag

Listing 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>

6. Screen 2: Climate Tab

Identical pattern to Highlights, with type = climate.

6.1 Load Climate Section

GET /destinations/:id/sections/climate

6.2 Save Climate Section

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>"
}

6.3 Upload Image in Climate Editor

POST /destinations/:id/sections/climate/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>

7. Screen 2: Getting There Tab

Identical pattern, with type = getting_there.

7.1 Load Getting There Section

GET /destinations/:id/sections/getting_there
Authorization: Bearer <jwt_token>

Response:

{
  "message": "SUC_SECTION_DETAILS",
  "data": {
    "content": "<p>France is well-connected by air, rail, and road. Major airports include Charles de Gaulle (Paris) and Nice Cote d'Azur.</p>",
    "media": []
  }
}

Frontend Mapping:


7.2 Save Getting There Section

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>"
}

7.3 Upload Image in Getting There Editor

POST /destinations/:id/sections/getting_there/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>

8. Screen 2: Docs & Health Tab

Identical pattern, with type = docs_health.

8.1 Load Docs & Health Section

GET /destinations/:id/sections/docs_health
Authorization: Bearer <jwt_token>

Response:

{
  "message": "SUC_SECTION_DETAILS",
  "data": {
    "content": "<p>No visa required for EU citizens. Schengen visa for non-EU visitors.</p>",
    "media": []
  }
}

Frontend Mapping:


8.2 Save Docs & Health Section

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>"
}

8.3 Upload Image in Docs & Health Editor

POST /destinations/:id/sections/docs_health/media
Content-Type: multipart/form-data
Authorization: Bearer <jwt_token>

9. Common Patterns

9.1 Load Full Destination (Edit Mode)

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, "image": null },
      { "id": 2, "key": "Language", "value": "French", "order": 2, "image": null },
      { "id": 3, "key": "Best Time to Travel", "value": "April to June", "order": 3, "image": "https://cdn.example.com/icons/calendar.png" }
    ],
    "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. The media array contains all uploaded images for that section.


9.2 Tab Switching Strategy

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
Climate GET /destinations/:id/sections/climate
Getting There GET /destinations/:id/sections/getting_there
Docs & Health GET /destinations/:id/sections/docs_health

Optimization: Cache section data in component state so revisiting a tab doesn't refetch.


9.3 Authentication Header

All write endpoints (POST, PUT, PATCH, DELETE) require:

Authorization: Bearer <jwt_access_token>

The user must have Permission.DESTINATION = 18 assigned to their role.


9.4 Image Upload Flow in Rich Text Editor

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

10. Error Handling

10.1 Common Error Codes

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

10.2 Generic Error Response Shape

{
  "statusCode": 400,
  "message": "ERR_INVALID_PARENT_COUNTRY"
}

10.3 Frontend Error Handling Checklist


11. i18n Message Reference

All API responses use message codes that map to human-readable strings in src/i18n/en/success.json and src/i18n/en/error.json.

Success Messages

Code Message
SUC_DESTINATION_CREATED Destination created successfully.
SUC_DESTINATION_LIST Destination list fetched successfully.
SUC_DESTINATION_DETAILS Destination details fetched successfully.
SUC_DESTINATION_UPDATED Destination updated successfully.
SUC_DESTINATION_DELETED Destination deleted successfully.
SUC_DESTINATION_STATUS_UPDATED Destination status updated successfully.
SUC_PARENT_OPTIONS Parent options fetched successfully.
SUC_DESTINATION_FULL Destination details with overview and sections fetched successfully.
SUC_OVERVIEW_UPDATED Overview facts updated successfully.
SUC_OVERVIEW_DETAILS Overview facts fetched successfully.
SUC_SECTION_UPDATED Section content updated successfully.
SUC_SECTION_DETAILS Section details fetched successfully.
SUC_SECTION_MEDIA_UPLOADED Section media uploaded successfully.
SUC_SECTION_MEDIA_LIST Section media list fetched successfully.
SUC_SECTION_MEDIA_DELETED Section media deleted successfully.

Error Messages

Code Message
ERR_DESTINATION_NOT_FOUND Destination not found.
ERR_INVALID_PARENT_WORLD_AREA Invalid parent. World areas cannot have a parent, and countries must reference a valid world area.
ERR_INVALID_PARENT_COUNTRY Invalid parent. Regions must reference a valid country.
ERR_DESTINATION_NAME_EXISTS A destination with this name already exists.
ERR_INVALID_SECTION_TYPE Invalid section type. Must be one of: highlights, climate, getting_there, docs_health.
ERR_MEDIA_FILE_REQUIRED Media file is required for upload.
ERR_INVALID_TYPE Invalid destination type.

Appendix A: Full API Summary

Public Endpoints (No Auth Required)

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

Authenticated Endpoints (JWT + DESTINATION Permission)

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

Valid Section Types

:type can only be one of:


End of Frontend Integration Guide