Studi Kasus #6: Aplikasi Wisata Indonesia – Membuat API untuk Penginapan (Accommodations)


Studi Kasus #6: Aplikasi Wisata Indonesia – Membuat API untuk Penginapan (Accommodations)

Halo, calon full-stack developer! 👋

Di Studi Kasus #5, kita telah membuat API untuk tempat wisata (destinations). Sekarang kita akan membuat API untuk penginapan (accommodations) yang strukturnya mirip, namun dengan fokus pada data penginapan seperti hotel, villa, homestay, dll. Penginapan juga memiliki kategori, fasilitas, dan gambar cover.

Fitur yang akan kita buat:

  • CRUD lengkap untuk data penginapan.
  • Relasi dengan kategori (setiap penginapan memiliki satu kategori).
  • Upload gambar cover menggunakan multer.
  • Mengambil data penginapan beserta informasi kategori (join).

Kita akan menguji semua endpoint menggunakan Postman. Mari kita mulai! 🚀

📁 Struktur Folder dan File

Kita akan menambahkan file baru di folder controllers dan routes:

backend/
├── controllers/
│   ├── accommodationController.js  (baru)
│   └── ...
├── routes/
│   ├── accommodationRoutes.js      (baru)
│   └── ...
├── middlewares/
│   └── upload.js                   (sudah ada)
├── uploads/                         (sudah ada)
└── ...

Middleware upload.js sudah kita buat sebelumnya dan akan digunakan kembali.

Membuat Accommodation Controller

Buat file controllers/accommodationController.js. Kodenya sangat mirip dengan destinationController, hanya berbeda nama tabel dan field.

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

// GET semua penginapan (dengan informasi kategori)
const getAllAccommodations = async (req, res) => {
  try {
    const query = `
      SELECT a.*, c.name as category_name, c.slug as category_slug
      FROM accommodations a
      LEFT JOIN categories c ON a.category_id = c.id
      ORDER BY a.id DESC
    `;
    const [rows] = await db.query(query);
    res.json(rows);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data penginapan' });
  }
};

// GET satu penginapan berdasarkan ID (dengan kategori)
const getAccommodationById = async (req, res) => {
  const { id } = req.params;
  try {
    const query = `
      SELECT a.*, c.name as category_name, c.slug as category_slug
      FROM accommodations a
      LEFT JOIN categories c ON a.category_id = c.id
      WHERE a.id = ?
    `;
    const [rows] = await db.query(query, [id]);
    if (rows.length === 0) {
      return res.status(404).json({ error: 'Penginapan tidak ditemukan' });
    }
    res.json(rows[0]);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal mengambil data penginapan' });
  }
};

// POST tambah penginapan baru (dengan upload cover)
const createAccommodation = async (req, res) => {
  const { category_id, name, slug, location, description, facilities } = req.body;
  const coverImage = req.file ? req.file.filename : null;

  if (!category_id || !name || !slug || !location) {
    return res.status(400).json({ error: 'category_id, name, slug, dan location harus diisi' });
  }

  try {
    // Cek slug
    const [existing] = await db.query('SELECT id FROM accommodations WHERE slug = ?', [slug]);
    if (existing.length > 0) {
      return res.status(400).json({ error: 'Slug sudah digunakan, gunakan slug lain' });
    }

    const [result] = await db.query(
      `INSERT INTO accommodations (category_id, name, slug, location, description, facilities, cover_image)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [category_id, name, slug, location, description, facilities, coverImage]
    );

    const [newAccommodation] = await db.query(
      `SELECT a.*, c.name as category_name
       FROM accommodations a
       LEFT JOIN categories c ON a.category_id = c.id
       WHERE a.id = ?`,
      [result.insertId]
    );

    res.status(201).json(newAccommodation[0]);
  } catch (err) {
    console.error(err);
    if (req.file) {
      fs.unlinkSync(path.join(__dirname, '../uploads', req.file.filename));
    }
    res.status(500).json({ error: 'Gagal menambah penginapan' });
  }
};

// PUT update penginapan
const updateAccommodation = async (req, res) => {
  const { id } = req.params;
  const { category_id, name, slug, location, description, facilities } = req.body;
  const coverImage = req.file ? req.file.filename : null;

  try {
    const [oldData] = await db.query('SELECT cover_image FROM accommodations WHERE id = ?', [id]);
    if (oldData.length === 0) {
      return res.status(404).json({ error: 'Penginapan tidak ditemukan' });
    }

    if (slug) {
      const [existing] = await db.query('SELECT id FROM accommodations WHERE slug = ? AND id != ?', [slug, id]);
      if (existing.length > 0) {
        return res.status(400).json({ error: 'Slug sudah digunakan, gunakan slug lain' });
      }
    }

    let fields = [];
    let values = [];

    if (category_id) { fields.push('category_id = ?'); values.push(category_id); }
    if (name) { fields.push('name = ?'); values.push(name); }
    if (slug) { fields.push('slug = ?'); values.push(slug); }
    if (location) { fields.push('location = ?'); values.push(location); }
    if (description !== undefined) { fields.push('description = ?'); values.push(description); }
    if (facilities !== undefined) { fields.push('facilities = ?'); values.push(facilities); }
    if (coverImage) { fields.push('cover_image = ?'); values.push(coverImage); }

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

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

    if (result.affectedRows === 0) {
      return res.status(404).json({ error: 'Penginapan tidak ditemukan' });
    }

    if (coverImage && oldData[0].cover_image) {
      const oldPath = path.join(__dirname, '../uploads', oldData[0].cover_image);
      if (fs.existsSync(oldPath)) {
        fs.unlinkSync(oldPath);
      }
    }

    const [updated] = await db.query(
      `SELECT a.*, c.name as category_name
       FROM accommodations a
       LEFT JOIN categories c ON a.category_id = c.id
       WHERE a.id = ?`,
      [id]
    );
    res.json(updated[0]);
  } catch (err) {
    console.error(err);
    if (req.file) {
      fs.unlinkSync(path.join(__dirname, '../uploads', req.file.filename));
    }
    res.status(500).json({ error: 'Gagal mengupdate penginapan' });
  }
};

// DELETE hapus penginapan
const deleteAccommodation = async (req, res) => {
  const { id } = req.params;
  try {
    const [data] = await db.query('SELECT cover_image FROM accommodations WHERE id = ?', [id]);
    if (data.length === 0) {
      return res.status(404).json({ error: 'Penginapan tidak ditemukan' });
    }

    const [result] = await db.query('DELETE FROM accommodations WHERE id = ?', [id]);

    if (data[0].cover_image) {
      const filePath = path.join(__dirname, '../uploads', data[0].cover_image);
      if (fs.existsSync(filePath)) {
        fs.unlinkSync(filePath);
      }
    }

    res.json({ message: 'Penginapan berhasil dihapus' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal menghapus penginapan' });
  }
};

module.exports = {
  getAllAccommodations,
  getAccommodationById,
  createAccommodation,
  updateAccommodation,
  deleteAccommodation
};

Penjelasan:

  • Kode di atas hampir identik dengan destinationController, hanya mengganti tabel destinations menjadi accommodations.
  • Field yang digunakan: category_id, name, slug, location, description, facilities, cover_image sesuai dengan struktur tabel.
  • Semua fungsi menggunakan JOIN dengan tabel categories untuk mendapatkan nama kategori.

Membuat Accommodation Routes

Buat file routes/accommodationRoutes.js:

const express = require('express');
const router = express.Router();
const accommodationController = require('../controllers/accommodationController');
const upload = require('../middlewares/upload');

// GET semua penginapan
router.get('/', accommodationController.getAllAccommodations);

// GET satu penginapan
router.get('/:id', accommodationController.getAccommodationById);

// POST tambah penginapan (dengan upload cover)
router.post('/', upload.single('cover_image'), accommodationController.createAccommodation);

// PUT update penginapan (dengan upload cover opsional)
router.put('/:id', upload.single('cover_image'), accommodationController.updateAccommodation);

// DELETE hapus penginapan
router.delete('/:id', accommodationController.deleteAccommodation);

module.exports = router;

Di app.js, daftarkan route ini:

const accommodationRoutes = require('./routes/accommodationRoutes');
app.use('/api/accommodations', accommodationRoutes);

Jangan lupa juga untuk menyajikan folder uploads sebagai file statis (jika belum):

app.use('/uploads', express.static('uploads'));

🧪 Menguji API dengan Postman

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

1. GET Semua Penginapan

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

2. POST Tambah Penginapan (dengan gambar)

  • Method: POST
  • URL: http://localhost:3000/api/accommodations
  • Body: pilih form-data
  • Isi key-value:
    • category_id (text): 3 (misal kategori dengan id 3 adalah "Hotel")
    • name (text): Hotel Indonesia
    • slug (text): hotel-indonesia
    • location (text): Jakarta
    • description (text): Hotel bintang 5 di pusat kota.
    • facilities (text): Kolam renang, WiFi, Restoran
    • cover_image (file): pilih file gambar
  • Klik Send. Respons sukses akan mengembalikan data penginapan yang baru dibuat.

3. GET Satu Penginapan

  • Method: GET
  • URL: http://localhost:3000/api/accommodations/1
  • Klik Send. Pastikan data dan kategori muncul.

4. PUT Update Penginapan (dengan gambar baru)

  • Method: PUT
  • URL: http://localhost:3000/api/accommodations/1
  • Body: form-data
  • Ubah field name menjadi "Hotel Indonesia Baru", dan upload gambar baru (atau tidak).
  • Klik Send. Respons akan mengembalikan data terbaru.

5. DELETE Hapus Penginapan

  • Method: DELETE
  • URL: http://localhost:3000/api/accommodations/1
  • Klik Send. Respons: { "message": "Penginapan berhasil dihapus" }
  • Cek folder uploads, gambar juga harus ikut terhapus.
💡 Tips: Pastikan kategori dengan ID yang digunakan sudah ada di tabel categories.

Informasi Kamar dan Fasilitas

Pada tabel accommodations, kolom facilities kita gunakan untuk menyimpan fasilitas umum penginapan (misal "Kolam renang, WiFi, Sarapan"). Untuk informasi kamar (seperti tipe kamar, harga, ketersediaan), kita akan menggunakan tabel packages yang sudah direncanakan. Nanti di tutorial terpisah kita akan membuat API untuk packages yang bisa dihubungkan ke penginapan.

Untuk sementara, fasilitas umum sudah cukup. Jika ingin menambahkan informasi kamar, kamu bisa menambahkannya sebagai JSON di kolom facilities atau membuat tabel terpisah. Kita akan bahas di studi kasus selanjutnya.

Penanganan Error

  • 400 Bad Request: Field wajib tidak diisi, slug duplikat, atau file bukan gambar.
  • 404 Not Found: ID penginapan tidak ditemukan.
  • 500 Internal Server Error: Kesalahan server.

Langkah Selanjutnya

Di Studi Kasus #7, kita akan membuat API untuk Transportasi (Transportations) dengan pola yang sama. Setelah itu, kita akan mulai membuat API untuk galeri dan paket harga.

Pastikan semua endpoint penginapan berfungsi dengan baik. Jika ada error, periksa kembali kode, terutama nama tabel dan field.

Sampai jumpa di tutorial berikutnya! 👋😊


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

Lebih baru Lebih lama

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