Studi Kasus #9: Aplikasi Wisata Indonesia – Membuat API untuk Paket dan Harga (Packages)


Studi Kasus #9: Aplikasi Wisata Indonesia – Membuat API untuk Paket dan Harga (Packages)

Halo, calon full-stack developer!

Di Studi Kasus #8, kita telah membuat API untuk galeri foto. Sekarang kita akan membuat API untuk paket dan harga (packages). Paket ini bisa berupa tiket masuk tempat wisata, tipe kamar hotel, atau layanan transportasi beserta harganya. Dengan API ini, kita dapat mengelola berbagai penawaran untuk setiap entitas.

Fitur yang akan dibuat:

  • CRUD lengkap untuk data paket.
  • Relasi dengan entitas (destinasi, penginapan, transportasi) menggunakan entity_type dan entity_id.
  • Mengambil daftar paket berdasarkan entitas.
  • Validasi agar paket hanya bisa ditambahkan ke entitas yang valid.

Kita akan menggunakan tabel packages yang sudah dirancang di awal. Mari kita mulai! 🚀

📁 Struktur Folder dan File

Kita akan menambahkan file baru di folder controllers dan routes untuk packages:

backend/
├── controllers/
│   ├── packageController.js          (baru)
│   └── ...
├── routes/
│   ├── packageRoutes.js              (baru)
│   └── ...
└── ...

Skema Tabel Packages

Ingat kembali tabel packages yang kita buat di Studi Kasus #1:

CREATE TABLE packages (
  id INT AUTO_INCREMENT PRIMARY KEY,
  entity_type ENUM('destination', 'accommodation', 'transportation') NOT NULL,
  entity_id INT NOT NULL,
  name VARCHAR(200) NOT NULL,
  description TEXT,
  price DECIMAL(10,2) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
  • entity_type menentukan jenis entitas (destination, accommodation, transportation).
  • entity_id adalah ID dari entitas tersebut.
  • name nama paket (misal: "Tiket Masuk Dewasa", "Kamar Deluxe").
  • description deskripsi paket (opsional).
  • price harga paket.

Membuat Package Controller

Buat file controllers/packageController.js dengan kode berikut:

const db = require('../config/db');

// GET semua paket (dengan informasi entitas? Bisa ditambahkan nanti)
const getAllPackages = async (req, res) => {
  try {
    const [rows] = await db.query('SELECT * FROM packages ORDER BY id DESC');
    res.json(rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data paket' });
  }
};

// GET paket berdasarkan entity_type dan entity_id
const getPackagesByEntity = async (req, res) => {
  const { entity_type, entity_id } = req.params;

  const validTypes = ['destination', 'accommodation', 'transportation'];
  if (!validTypes.includes(entity_type)) {
    return res.status(400).json({ error: 'entity_type tidak valid' });
  }

  try {
    const [rows] = await db.query(
      'SELECT * FROM packages WHERE entity_type = ? AND entity_id = ? ORDER BY price ASC',
      [entity_type, entity_id]
    );
    res.json(rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data paket' });
  }
};

// GET satu paket berdasarkan ID
const getPackageById = async (req, res) => {
  const { id } = req.params;
  try {
    const [rows] = await db.query('SELECT * FROM packages WHERE id = ?', [id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Paket tidak ditemukan' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data paket' });
  }
};

// POST tambah paket baru
const createPackage = async (req, res) => {
  const { entity_type, entity_id, name, description, price } = req.body;

  // Validasi
  const validTypes = ['destination', 'accommodation', 'transportation'];
  if (!validTypes.includes(entity_type)) {
    return res.status(400).json({ error: 'entity_type tidak valid' });
  }
  if (!entity_id || !name || !price) {
    return res.status(400).json({ error: 'entity_id, name, dan price harus diisi' });
  }
  if (isNaN(price) || price <= 0) {
    return res.status(400).json({ error: 'Harga harus angka positif' });
  }

  // Cek apakah entitas dengan ID tersebut ada
  try {
    const [rows] = await db.query(`SELECT id FROM ${entity_type}s WHERE id = ?`, [entity_id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: `${entity_type} dengan ID ${entity_id} tidak ditemukan` });
    }
  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: 'Gagal memverifikasi entitas' });
  }

  try {
    const [result] = await db.query(
      'INSERT INTO packages (entity_type, entity_id, name, description, price) VALUES (?, ?, ?, ?, ?)',
      [entity_type, entity_id, name, description, price]
    );

    const [newPackage] = await db.query('SELECT * FROM packages WHERE id = ?', [result.insertId]);
    res.status(201).json(newPackage[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal menambah paket' });
  }
};

// PUT update paket
const updatePackage = async (req, res) => {
  const { id } = req.params;
  const { name, description, price } = req.body;

  try {
    // Cek apakah paket ada
    const [existing] = await db.query('SELECT * FROM packages WHERE id = ?', [id]);
    if (existing.length === 0) {
      return res.status(404).json({ error: 'Paket tidak ditemukan' });
    }

    // Validasi price jika ada
    if (price !== undefined && (isNaN(price) || price <= 0)) {
      return res.status(400).json({ error: 'Harga harus angka positif' });
    }

    // Bangun query dinamis
    let fields = [];
    let values = [];
    if (name !== undefined) { fields.push('name = ?'); values.push(name); }
    if (description !== undefined) { fields.push('description = ?'); values.push(description); }
    if (price !== undefined) { fields.push('price = ?'); values.push(price); }

    if (fields.length === 0) {
      return res.status(400).json({ error: 'Tidak ada data yang diupdate' });
    }

    values.push(id);
    const query = `UPDATE packages SET ${fields.join(', ')} WHERE id = ?`;
    await db.query(query, values);

    const [updated] = await db.query('SELECT * FROM packages WHERE id = ?', [id]);
    res.json(updated[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengupdate paket' });
  }
};

// DELETE hapus paket
const deletePackage = async (req, res) => {
  const { id } = req.params;
  try {
    const [result] = await db.query('DELETE FROM packages WHERE id = ?', [id]);
    if (result.affectedRows === 0) {
      return res.status(404).json({ error: 'Paket tidak ditemukan' });
    }
    res.json({ message: 'Paket berhasil dihapus' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal menghapus paket' });
  }
};

module.exports = {
  getAllPackages,
  getPackagesByEntity,
  getPackageById,
  createPackage,
  updatePackage,
  deletePackage
};

Penjelasan:

  • getAllPackages: Mengambil semua paket (untuk keperluan admin).
  • getPackagesByEntity: Mengambil paket berdasarkan entity_type dan entity_id (akan digunakan di halaman detail).
  • getPackageById: Mengambil satu paket berdasarkan ID.
  • createPackage: Menambah paket baru, dengan validasi entity dan harga.
  • updatePackage: Mengupdate paket (hanya field yang dikirim).
  • deletePackage: Menghapus paket.

Membuat Package Routes

Buat file routes/packageRoutes.js:

const express = require('express');
const router = express.Router();
const packageController = require('../controllers/packageController');

// GET semua paket (opsional, untuk admin)
router.get('/', packageController.getAllPackages);

// GET paket berdasarkan entity (contoh: /api/packages/destination/5)
router.get('/:entity_type/:entity_id', packageController.getPackagesByEntity);

// GET satu paket berdasarkan ID
router.get('/:id', packageController.getPackageById);

// POST tambah paket
router.post('/', packageController.createPackage);

// PUT update paket
router.put('/:id', packageController.updatePackage);

// DELETE hapus paket
router.delete('/:id', packageController.deletePackage);

module.exports = router;

Di app.js, daftarkan route ini:

const packageRoutes = require('./routes/packageRoutes');
app.use('/api/packages', packageRoutes);

🧪 Menguji API dengan Postman

Pastikan server berjalan (node app.js). Kita akan menguji endpoint packages.

1. GET Semua Paket

  • Method: GET
  • URL: http://localhost:3000/api/packages
  • Klik Send. Respons awal mungkin array kosong.

2. POST Tambah Paket untuk Destinasi

  • Method: POST
  • URL: http://localhost:3000/api/packages
  • Headers: Content-Type: application/json
  • Body (raw JSON):
    {
      "entity_type": "destination",
      "entity_id": 1,
      "name": "Tiket Masuk Dewasa",
      "description": "Tiket masuk untuk pengunjung dewasa",
      "price": 25000
    }
  • Klik Send. Respons sukses mengembalikan data paket yang baru dibuat.

3. GET Paket Berdasarkan Entitas

  • Method: GET
  • URL: http://localhost:3000/api/packages/destination/1
  • Klik Send. Akan menampilkan daftar paket untuk destinasi ID 1.

4. GET Satu Paket Berdasarkan ID

  • Method: GET
  • URL: http://localhost:3000/api/packages/1 (ganti dengan ID yang baru)
  • Klik Send.

5. PUT Update Paket

  • Method: PUT
  • URL: http://localhost:3000/api/packages/1
  • Body (JSON):
    {
      "name": "Tiket Masuk Dewasa (Weekend)",
      "price": 30000
    }
  • Klik Send. Respons mengembalikan data yang sudah diupdate.

6. DELETE Hapus Paket

  • Method: DELETE
  • URL: http://localhost:3000/api/packages/1
  • Klik Send. Respons: { "message": "Paket berhasil dihapus" }

7. Tambah Paket untuk Penginapan

Ulangi langkah 2 dengan entity_type "accommodation". Contoh:

{
  "entity_type": "accommodation",
  "entity_id": 1,
  "name": "Kamar Deluxe",
  "description": "Kamar dengan pemandangan laut",
  "price": 750000
}

8. Tambah Paket untuk Transportasi

Contoh untuk transportasi:

{
  "entity_type": "transportation",
  "entity_id": 1,
  "name": "Sewa Mobil + Sopir",
  "description": "Mobil Avanza dengan sopir, 10 jam",
  "price": 500000
}
💡 Tips: Pastikan entity_id yang digunakan sudah ada di tabel terkait. Jika belum, tambahkan data dummy terlebih dahulu.

Penanganan Error

  • 400 Bad Request: Jika entity_type tidak valid, field wajib kosong, atau harga bukan angka positif.
  • 404 Not Found: Jika entitas tidak ditemukan, atau ID paket tidak ditemukan saat update/delete.
  • 500 Internal Server Error: Kesalahan database.

Langkah Selanjutnya

Dengan selesainya API packages, kita sekarang memiliki backend yang lengkap untuk mengelola data wisata, penginapan, transportasi, galeri, dan paket harga. Di Studi Kasus #10, kita akan menambahkan autentikasi JWT untuk admin agar halaman admin dapat diamankan.

Pastikan semua endpoint packages berfungsi dengan baik. Jika ada error, periksa kembali kode dan struktur database.

Sampai jumpa di tutorial berikutnya!


Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.

Lebih baru Lebih lama

نموذج الاتصال