Studi Kasus #7: Aplikasi Wisata Indonesia – Membuat API untuk Transportasi


Studi Kasus #7: Aplikasi Wisata Indonesia – Membuat API untuk Transportasi

Halo, calon full-stack developer! 

Di Studi Kasus #6, kita telah membuat API untuk penginapan. Sekarang kita akan membuat API untuk transportasi. Modul transportasi ini akan menyediakan data berbagai layanan transportasi seperti travel, rental mobil, bus pariwisata, dll. Strukturnya mirip dengan destinasi dan penginapan, dengan relasi ke tabel kategori.

Fitur yang akan kita buat:

  • CRUD lengkap untuk data transportasi.
  • Relasi dengan kategori (setiap transportasi memiliki satu kategori).
  • Upload gambar cover menggunakan multer.
  • Mengambil data transportasi 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/
│   ├── transportationController.js  (baru)
│   └── ...
├── routes/
│   ├── transportationRoutes.js      (baru)
│   └── ...
├── middlewares/
│   └── upload.js                    (sudah ada)
├── uploads/                          (sudah ada)
└── ...

Middleware upload.js akan digunakan kembali.

Skema Tabel Transportations

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

CREATE TABLE transportations (
  id INT AUTO_INCREMENT PRIMARY KEY,
  category_id INT NOT NULL,
  name VARCHAR(200) NOT NULL,
  slug VARCHAR(200) NOT NULL UNIQUE,
  location VARCHAR(255) NOT NULL, -- misal lokasi kantor atau rute
  description TEXT,
  facilities TEXT, -- fasilitas kendaraan
  cover_image VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
  • location bisa diisi dengan lokasi kantor, daerah operasi, atau rute umum.
  • facilities untuk fasilitas kendaraan (misal: AC, WiFi, Toilet).

Membuat Transportation Controller

Buat file controllers/transportationController.js. Kodenya sangat mirip dengan accommodationController, hanya mengganti nama tabel.

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

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

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

// POST tambah transportasi baru (dengan upload cover)
const createTransportation = 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 transportations 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 transportations (category_id, name, slug, location, description, facilities, cover_image)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [category_id, name, slug, location, description, facilities, coverImage]
    );

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

    res.status(201).json(newTransportation[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 transportasi' });
  }
};

// PUT update transportasi
const updateTransportation = 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 transportations WHERE id = ?', [id]);
    if (oldData.length === 0) {
      return res.status(404).json({ error: 'Transportasi tidak ditemukan' });
    }

    if (slug) {
      const [existing] = await db.query('SELECT id FROM transportations 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 transportations SET ${fields.join(', ')} WHERE id = ?`;
    const [result] = await db.query(query, values);

    if (result.affectedRows === 0) {
      return res.status(404).json({ error: 'Transportasi 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 t.*, c.name as category_name
       FROM transportations t
       LEFT JOIN categories c ON t.category_id = c.id
       WHERE t.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 transportasi' });
  }
};

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

    const [result] = await db.query('DELETE FROM transportations 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: 'Transportasi berhasil dihapus' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Gagal menghapus transportasi' });
  }
};

module.exports = {
  getAllTransportations,
  getTransportationById,
  createTransportation,
  updateTransportation,
  deleteTransportation
};

Penjelasan:

  • Kode di atas identik dengan controller sebelumnya, hanya nama tabel yang diganti menjadi transportations.
  • Semua fungsi menggunakan JOIN dengan tabel categories untuk mendapatkan nama kategori.
  • Penanganan file upload dan hapus file sama seperti sebelumnya.

Membuat Transportation Routes

Buat file routes/transportationRoutes.js:

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

// GET semua transportasi
router.get('/', transportationController.getAllTransportations);

// GET satu transportasi
router.get('/:id', transportationController.getTransportationById);

// POST tambah transportasi (dengan upload cover)
router.post('/', upload.single('cover_image'), transportationController.createTransportation);

// PUT update transportasi (dengan upload cover opsional)
router.put('/:id', upload.single('cover_image'), transportationController.updateTransportation);

// DELETE hapus transportasi
router.delete('/:id', transportationController.deleteTransportation);

module.exports = router;

Di app.js, daftarkan route ini:

const transportationRoutes = require('./routes/transportationRoutes');
app.use('/api/transportations', transportationRoutes);

Pastikan middleware express.static('uploads') sudah ada untuk mengakses gambar.

Menguji API dengan Postman

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

1. GET Semua Transportasi

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

2. POST Tambah Transportasi (dengan gambar)

  • Method: POST
  • URL: http://localhost:3000/api/transportations
  • Body: pilih form-data
  • Isi key-value:
    • category_id (text): 5 (misal kategori "Travel")
    • name (text): Travel Aman
    • slug (text): travel-aman
    • location (text): Jakarta - Bandung
    • description (text): Layanan travel antar kota dengan armada nyaman.
    • facilities (text): AC, WiFi, Makan ringan
    • cover_image (file): pilih file gambar
  • Klik Send. Respons sukses akan mengembalikan data transportasi yang baru dibuat.

3. GET Satu Transportasi

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

4. PUT Update Transportasi (dengan gambar baru)

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

5. DELETE Hapus Transportasi

  • Method: DELETE
  • URL: http://localhost:3000/api/transportations/1
  • Klik Send. Respons: { "message": "Transportasi berhasil dihapus" }
  • Cek folder uploads, gambar juga harus ikut terhapus.
💡 Tips: Pastikan kategori dengan ID yang digunakan sudah ada di tabel categories (misal id 5 untuk "Travel").

Mengakses Gambar

Setelah upload, gambar dapat diakses melalui URL: http://localhost:3000/uploads/nama-file.jpg.

Penanganan Error

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

Langkah Selanjutnya

Dengan selesainya API untuk destinasi, penginapan, dan transportasi, kita sudah memiliki fondasi backend yang kuat. Di Studi Kasus #8, kita akan membuat API untuk Mengelola Galeri Foto (upload multiple gambar) yang bisa dihubungkan ke entitas mana pun (destinasi, penginapan, transportasi).

Pastikan semua endpoint transportasi 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

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