Destination Module — Frontend Integration Guide

Version: 1.1
Backend Phase: 01-destination-module (Complete)
Last Updated: 2026-05-11

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: Signature Experiences Tab
  6. Screen 2: Highlights Tab
  7. Screen 2: Travel Gems Tab
  8. Screen 2: Climate Tab (Structured)
  9. Screen 2: Getting There Tab
  10. Screen 2: Docs & Health Tab
  11. Screen 2: Sample Itinerary Tab
  12. Screen 2: Practical Information Tab
  13. Common Patterns
  14. Error Handling
  15. i18n Message Reference
  16. 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 Signature Experiences PUT /destinations/:id/signature-experiences 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 Travel Gems PUT /destinations/:id/sections/travel_gems Auth
Edit Destination Save Climate (structured) PUT /destinations/:id/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
Edit Destination Save Sample Itinerary PUT /destinations/:id/sample-itinerary Auth
Edit Destination Save Practical Information PUT /destinations/:id/practical-information 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
additionalImage File No Additional 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("additionalImage", additionalFileInput.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",
    "additionalImage": "https://cdn.example.com/france-additional.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
additionalImage File No Additional 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("additionalImage", additionalFileInput.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",
    "additionalImage": "https://cdn.example.com/france-additional-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: Signature Experiences Tab

5.1 Load Signature Experiences

When: User clicks the Signature Experiences tab.

API:

GET /destinations/:id/signature-experiences

Response:

{
  "message": "SUC_SIGNATURE_EXPERIENCE_DETAILS",
  "data": {
    "id": 1,
    "destinationId": 2,
    "heading": "Unforgettable Experiences",
    "subHeading": "Hand-picked activities for your journey",
    "description": "<p>Explore the best of this destination...</p>",
    "experiences": [
      {
        "id": 1,
        "title": "Hot Air Balloon Safari",
        "shortDescription": "Soar over the savannah at sunrise.",
        "image": "https://cdn.example.com/safari.jpg",
        "order": 1
      },
      {
        "id": 2,
        "title": "Private Wine Tasting",
        "shortDescription": "Sample the finest local vintages.",
        "image": null,
        "order": 2
      }
    ]
  }
}

If not created yet:

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

Frontend Mapping:


5.2 Save Signature Experiences

When: User clicks Save in the Signature Experiences tab.

API:

PUT /destinations/:id/signature-experiences
Content-Type: application/json
Authorization: Bearer <jwt_token>

Request Body:

{
  "heading": "Unforgettable Experiences",
  "subHeading": "Hand-picked activities for your journey",
  "description": "<p>Explore the best of this destination...</p>",
  "experiences": [
    {
      "id": 1,
      "title": "Hot Air Balloon Safari",
      "shortDescription": "Soar over the savannah at sunrise.",
      "image": "https://cdn.example.com/safari.jpg",
      "order": 1
    },
    {
      "title": "Private Wine Tasting",
      "shortDescription": "Sample the finest local vintages.",
      "image": null,
      "order": 2
    }
  ]
}

Important Notes:

Response:

{
  "message": "SUC_SIGNATURE_EXPERIENCE_UPDATED",
  "data": {
    "id": 1,
    "destinationId": 2
  }
}

6. Screen 2: Highlights Tab

Identical pattern to existing Highlights section.

6.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:


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

6.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>

7. Screen 2: Travel Gems Tab

Identical pattern to Highlights, with type = travel_gems.

7.1 Load Travel Gems Section

GET /destinations/:id/sections/travel_gems

7.2 Save Travel Gems Section

PUT /destinations/:id/sections/travel_gems
Content-Type: application/json
Authorization: Bearer <jwt_token>

Request Body:

{
  "content": "<p>Hidden gems and local secrets of this destination...</p>"
}

7.3 Upload Image in Travel Gems Editor

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

8. Screen 2: Climate Tab (Structured)

Note: The wireframe shows Climate as a structured form (not rich text). The old rich-text Climate section API (/sections/climate) still exists for backward compatibility, but the frontend should use the new structured API below.

8.1 Load Climate Data

When: User clicks the Climate tab.

API:

GET /destinations/:id/climate

Response:

{
  "message": "SUC_CLIMATE_DETAILS",
  "data": {
    "id": 1,
    "destinationId": 2,
    "subHeading": "When to visit",
    "seasons": [
      {
        "id": 1,
        "type": "peak_season",
        "fromMonth": "Jan",
        "toMonth": "Mar",
        "shortDescription": "The driest months. Brilliant sunshine, calm lagoons, excellent visibility for diving and snorkelling.",
        "order": 1
      },
      {
        "id": 2,
        "type": "excellent_value",
        "fromMonth": "Feb",
        "toMonth": "Apr",
        "shortDescription": "Shoulder season with great deals and fewer crowds.",
        "order": 2
      },
      {
        "id": 3,
        "type": "wet_season",
        "fromMonth": "May",
        "toMonth": "Aug",
        "shortDescription": "Monsoon rains bring lush greenery. Best for surfing.",
        "order": 3
      },
      {
        "id": 4,
        "type": "hidden_gem",
        "fromMonth": "Sep",
        "toMonth": "Nov",
        "shortDescription": "Quiet beaches and stunning sunsets.",
        "order": 4
      }
    ]
  }
}

If not created yet:

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

Frontend Mapping:


8.2 Save Climate Data

When: User clicks Save in the Climate tab.

API:

PUT /destinations/:id/climate
Content-Type: application/json
Authorization: Bearer <jwt_token>

Request Body:

{
  "subHeading": "When to visit",
  "seasons": [
    {
      "id": 1,
      "type": "peak_season",
      "fromMonth": "Jan",
      "toMonth": "Mar",
      "shortDescription": "The driest months...",
      "order": 1
    },
    {
      "type": "excellent_value",
      "fromMonth": "Feb",
      "toMonth": "Apr",
      "shortDescription": "Shoulder season...",
      "order": 2
    }
  ]
}

Important Notes:

Response:

{
  "message": "SUC_CLIMATE_UPDATED",
  "data": {
    "id": 1,
    "destinationId": 2
  }
}

9. Screen 2: Getting There Tab

Identical pattern to Highlights, with type = getting_there.

9.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:


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

9.3 Upload Image in Getting There Editor

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

10. Screen 2: Docs & Health Tab

Identical pattern, with type = docs_health.

10.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:


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

10.3 Upload Image in Docs & Health Editor

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

11. Screen 2: Sample Itinerary Tab

11.1 Load Sample Itinerary

When: User clicks the Sample Itinerary tab.

API:

GET /destinations/:id/sample-itinerary

Response:

{
  "message": "SUC_SAMPLE_ITINERARY_DETAILS",
  "data": {
    "id": 1,
    "destinationId": 2,
    "heading": "7-Day Luxury Escape",
    "subHeading": "A curated week of wonder",
    "itineraryImage": "https://cdn.example.com/itinerary.jpg",
    "items": [
      {
        "id": 1,
        "heading": "Day 1: Arrival",
        "subHeading": "Welcome to paradise",
        "shortDescription": "Arrive at the airport and transfer to your hotel.",
        "order": 1
      },
      {
        "id": 2,
        "heading": "Day 2: Island Hopping",
        "subHeading": "Explore the atolls",
        "shortDescription": "Speedboat tour of nearby islands with picnic lunch.",
        "order": 2
      }
    ]
  }
}

If not created yet:

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

Frontend Mapping:


11.2 Save Sample Itinerary

When: User clicks Save in the Sample Itinerary tab.

API:

PUT /destinations/:id/sample-itinerary
Content-Type: application/json
Authorization: Bearer <jwt_token>

Request Body:

{
  "heading": "7-Day Luxury Escape",
  "subHeading": "A curated week of wonder",
  "itineraryImage": "https://cdn.example.com/itinerary.jpg",
  "items": [
    {
      "id": 1,
      "heading": "Day 1: Arrival",
      "subHeading": "Welcome to paradise",
      "shortDescription": "Arrive at the airport and transfer to your hotel.",
      "order": 1
    },
    {
      "heading": "Day 2: Island Hopping",
      "subHeading": "Explore the atolls",
      "shortDescription": "Speedboat tour of nearby islands with picnic lunch.",
      "order": 2
    }
  ]
}

Important Notes:

Response:

{
  "message": "SUC_SAMPLE_ITINERARY_UPDATED",
  "data": {
    "id": 1,
    "destinationId": 2
  }
}

12. Screen 2: Practical Information Tab

12.1 Load Practical Information

When: User clicks the Practical Information tab.

API:

GET /destinations/:id/practical-information

Response:

{
  "message": "SUC_PRACTICAL_INFORMATION_DETAILS",
  "data": {
    "id": 1,
    "destinationId": 2,
    "rows": [
      {
        "id": 1,
        "gettingThere": "Direct flights London Heathrow to Male ~10-11 hours",
        "entryRequirements": "No visa required for UK passport holders",
        "healthSafety": "No mandatory vaccinations required",
        "currencyCosts": "Local currency is Maldivian Rufiyaa (MVR)",
        "order": 1
      },
      {
        "id": 2,
        "gettingThere": "Seaplane transfer from Male to resort ~45 min",
        "entryRequirements": "Tourist visa on arrival for most nationalities",
        "healthSafety": "Travel insurance recommended",
        "currencyCosts": "US Dollars widely accepted",
        "order": 2
      }
    ]
  }
}

If not created yet:

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

Frontend Mapping:


12.2 Save Practical Information

When: User clicks Save in the Practical Information tab.

API:

PUT /destinations/:id/practical-information
Content-Type: application/json
Authorization: Bearer <jwt_token>

Request Body:

{
  "rows": [
    {
      "id": 1,
      "gettingThere": "Direct flights London Heathrow to Male ~10-11 hours",
      "entryRequirements": "No visa required for UK passport holders",
      "healthSafety": "No mandatory vaccinations required",
      "currencyCosts": "Local currency is Maldivian Rufiyaa (MVR)",
      "order": 1
    },
    {
      "gettingThere": "Seaplane transfer from Male to resort ~45 min",
      "entryRequirements": "Tourist visa on arrival for most nationalities",
      "healthSafety": "Travel insurance recommended",
      "currencyCosts": "US Dollars widely accepted",
      "order": 2
    }
  ]
}

Important Notes:

Response:

{
  "message": "SUC_PRACTICAL_INFORMATION_UPDATED",
  "data": {
    "id": 1,
    "destinationId": 2
  }
}

13. Common Patterns

13.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",
    "additionalImage": "https://cdn.example.com/france-additional.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/..." }
        ]
      },
      "travel_gems": null,
      "climate": null,
      "getting_there": null,
      "docs_health": null
    },
    "signatureExperience": {
      "id": 1,
      "heading": "Unforgettable Experiences",
      "subHeading": "Hand-picked activities",
      "description": "<p>Explore...</p>",
      "experiences": [
        { "id": 1, "title": "Safari", "shortDescription": "...", "image": null, "order": 1 }
      ]
    },
    "climate": {
      "id": 1,
      "subHeading": "When to visit",
      "seasons": [
        { "id": 1, "type": "peak_season", "fromMonth": "Jan", "toMonth": "Mar", "shortDescription": "...", "order": 1 }
      ]
    },
    "sampleItinerary": {
      "id": 1,
      "heading": "7-Day Escape",
      "subHeading": "A curated week",
      "itineraryImage": "https://cdn.example.com/itinerary.jpg",
      "items": [
        { "id": 1, "heading": "Day 1", "subHeading": "Arrival", "shortDescription": "...", "order": 1 }
      ]
    },
    "practicalInformation": {
      "id": 1,
      "rows": [
        { "id": 1, "gettingThere": "...", "entryRequirements": "...", "healthSafety": "...", "currencyCosts": "...", "order": 1 }
      ]
    }
  }
}

Note: The sections object keys are highlights, travel_gems, 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.

The top-level climate field is the structured climate data (from the new API). The sections.climate is the legacy rich-text section.


13.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
Signature Experiences GET /destinations/:id/signature-experiences
Highlights GET /destinations/:id/sections/highlights
Travel Gems GET /destinations/:id/sections/travel_gems
Climate (Structured) GET /destinations/:id/climate
Getting There GET /destinations/:id/sections/getting_there
Docs & Health GET /destinations/:id/sections/docs_health
Sample Itinerary GET /destinations/:id/sample-itinerary
Practical Information GET /destinations/:id/practical-information

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


13.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.


13.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

14. Error Handling

14.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, or 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_SLUG_EXISTS 409 Slug already used Show inline error on slug 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 allowed values Internal bug — should not happen
ERR_MEDIA_FILE_REQUIRED 400 Image upload called without a file Show "Please select a file" message

14.2 Generic Error Response Shape

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

14.3 Frontend Error Handling Checklist


15. 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_OVERVIEW_MEDIA_UPLOADED Overview image uploaded 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.
SUC_SIGNATURE_EXPERIENCE_DETAILS Signature experiences fetched successfully.
SUC_SIGNATURE_EXPERIENCE_UPDATED Signature experiences updated successfully.
SUC_CLIMATE_DETAILS Climate data fetched successfully.
SUC_CLIMATE_UPDATED Climate data updated successfully.
SUC_SAMPLE_ITINERARY_DETAILS Sample itinerary fetched successfully.
SUC_SAMPLE_ITINERARY_UPDATED Sample itinerary updated successfully.
SUC_PRACTICAL_INFORMATION_DETAILS Practical information fetched successfully.
SUC_PRACTICAL_INFORMATION_UPDATED Practical information updated 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_DESTINATION_SLUG_EXISTS A destination with this slug already exists.
ERR_INVALID_SECTION_TYPE Invalid section type. Must be one of: highlights, travel_gems, 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
GET /destinations/:id/signature-experiences Get signature experiences
GET /destinations/:id/climate Get structured climate data
GET /destinations/:id/sample-itinerary Get sample itinerary
GET /destinations/:id/practical-information Get practical information

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
PUT /destinations/:id/signature-experiences Upsert signature experiences
PUT /destinations/:id/climate Upsert structured climate data
PUT /destinations/:id/sample-itinerary Upsert sample itinerary
PUT /destinations/:id/practical-information Upsert practical information

Valid Section Types

:type can only be one of:


End of Frontend Integration Guide