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

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 },
    { "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:


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

Important Notes:

Response:

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

Frontend Action:


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


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

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

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 },
      { "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.


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


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