ASP.NET Core API #6: Membuat CRUD untuk Daftar Wisata dengan Relasi Kategori

ASP.NET Core API #6: Membuat CRUD untuk Daftar Wisata dengan Relasi Kategori

Halo lagi, calon programmer liburan! 👋

Di artikel sebelumnya kita sudah bisa mengelola Kategori Wisata dengan CRUD. Sekarang kita akan mengelola Daftar Wisata (tempat wisata) yang terhubung dengan kategori. Ibaratnya, kita sudah punya daftar jenis tempat (pantai, gunung, museum), sekarang kita isi dengan tempat-tempat aslinya. Misalnya Pantai Kuta termasuk kategori Pantai, Gunung Bromo termasuk kategori Gunung, dan seterusnya.

🤣 Kenapa tempat wisata suka bingung? Karena dia punya banyak relasi! (Canda dikit biar nggak tegang)

Mengingat Kembali Relasi

Di database, tabel DaftarWisata punya kolom KategoriId yang mengacu ke Id di tabel KategoriWisata. Artinya, setiap tempat wisata harus punya satu kategori. Sebaliknya, satu kategori bisa punya banyak tempat wisata. Nah, kita harus pastikan di API kita relasi ini tetap terjaga.

Persiapan: Mengatasi Masalah Loop Referensi

Di model kita, DaftarWisata punya properti Kategori (navigation property), dan KategoriWisata juga punya koleksi DaftarWisata (kalau kita tambahkan). Kalau tidak hati-hati, pas kita ambil data wisata, EF akan mencoba memuat kategori, lalu kategori memuat daftar wisata lagi, dan seterusnya bolak-balik kayak cermin. Bisa bikin pusing dan error!

Solusi gampang: kita kasih tahu serializer JSON agar mengabaikan loop. Caranya, tambahkan kode berikut di Program.cs (setelah AddControllers):

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
    });

Dengan ini, kalau ada loop, JSON akan berhenti dan tidak error. Anggap saja seperti kita bilang ke JSON: "Kalau ketemu lingkaran, jangan diterusin, berhenti aja."

Langkah 1: Membuat WisataController

Buat controller baru di folder Controllers dengan nama WisataController.cs. Klik kanan folder Controllers -> Add -> Controller -> pilih API Controller - Empty, beri nama WisataController.

Kita akan buat isinya mirip dengan KategoriController, tapi dengan tambahan Include untuk mengambil data kategori sekaligus.

Berikut kode lengkapnya (salin tempel aja, nanti kita jelaskan):

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WisataAPI.Data;
using WisataAPI.Models;

namespace WisataAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class WisataController : ControllerBase
    {
        private readonly WisataDbContext _context;

        public WisataController(WisataDbContext context)
        {
            _context = context;
        }

        // GET: api/wisata
        [HttpGet]
        public async Task<ActionResult<IEnumerable<DaftarWisata>>> GetWisata()
        {
            // Include Kategori supaya data kategori ikut terbawa
            return await _context.DaftarWisata
                .Include(w => w.Kategori)
                .ToListAsync();
        }

        // GET: api/wisata/5
        [HttpGet("{id}")]
        public async Task<ActionResult<DaftarWisata>> GetWisata(int id)
        {
            var wisata = await _context.DaftarWisata
                .Include(w => w.Kategori)
                .FirstOrDefaultAsync(w => w.Id == id);

            if (wisata == null)
            {
                return NotFound();
            }

            return wisata;
        }

        // POST: api/wisata
        [HttpPost]
        public async Task<ActionResult<DaftarWisata>> PostWisata(DaftarWisata wisata)
        {
            // Pastikan KategoriId yang dikirim ada di database
            var kategori = await _context.KategoriWisata.FindAsync(wisata.KategoriId);
            if (kategori == null)
            {
                return BadRequest("KategoriId tidak valid. Kategori tidak ditemukan.");
            }

            _context.DaftarWisata.Add(wisata);
            await _context.SaveChangesAsync();

            // Setelah simpan, kita panggil GET yang baru dibuat (dengan include)
            return CreatedAtAction(nameof(GetWisata), new { id = wisata.Id }, wisata);
        }

        // PUT: api/wisata/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutWisata(int id, DaftarWisata wisata)
        {
            if (id != wisata.Id)
            {
                return BadRequest("ID di URL tidak sama dengan ID di data.");
            }

            // Validasi KategoriId
            var kategori = await _context.KategoriWisata.FindAsync(wisata.KategoriId);
            if (kategori == null)
            {
                return BadRequest("KategoriId tidak valid. Kategori tidak ditemukan.");
            }

            _context.Entry(wisata).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!WisataExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // DELETE: api/wisata/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteWisata(int id)
        {
            var wisata = await _context.DaftarWisata.FindAsync(id);
            if (wisata == null)
            {
                return NotFound();
            }

            _context.DaftarWisata.Remove(wisata);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool WisataExists(int id)
        {
            return _context.DaftarWisata.Any(e => e.Id == id);
        }
    }
}

Wah, panjang lagi? Tenang, sebagian besar mirip dengan KategoriController. Yang baru adalah Include dan validasi KategoriId. Mari kita bedah satu per satu.

Penjelasan Method GET (dengan Include)

Di method GetWisata (tanpa parameter) kita pakai:

.Include(w => w.Kategori)

Ini perintah ke EF: "Tolong ambilkan juga data kategori yang berelasi dengan setiap wisata." Hasilnya, di response JSON kita akan lihat objek kategori di dalam setiap data wisata. Praktis kan? Jadi tidak hanya KategoriId, tapi kita juga tahu nama kategorinya.

Di method GetWisata(int id) kita pakai FirstOrDefaultAsync dengan Include yang sama.

Penjelasan Method POST

Method POST menerima data wisata baru. Tapi ada satu tantangan: kita harus memastikan KategoriId yang dikirim benar-benar ada di tabel Kategori. Kalau tidak, nanti akan error karena foreign key. Makanya kita lakukan pengecekan:

var kategori = await _context.KategoriWisata.FindAsync(wisata.KategoriId);
if (kategori == null)
{
    return BadRequest("KategoriId tidak valid. Kategori tidak ditemukan.");
}

Kalau tidak ditemukan, kita kembalikan 400 Bad Request dengan pesan yang jelas. Kalau valid, kita simpan data.

Setelah simpan, kita kembalikan CreatedAtAction yang memanggil method GetWisata (dengan include) sehingga response akan menyertakan data kategori juga.

Penjelasan Method PUT

Method PUT mirip dengan POST, tapi untuk update. Kita juga perlu validasi KategoriId. Kalau id di URL dan di body tidak cocok, kita tolak. Setelah itu kita set state menjadi Modified, lalu simpan.

Penjelasan Method DELETE

Sama seperti DELETE kategori, cari data, kalau ada hapus. Tidak ada validasi khusus karena foreign key sudah diurus database (kita tidak bisa menghapus kategori kalau masih ada wisata yang mengacu, tapi menghapus wisata itu aman).

Uji Coba di Postman

Sekarang kita coba semua endpoint. Pastikan API sudah running (F5).

1. GET semua wisata

  • Method: GET
  • URL: https://localhost:7000/api/wisata
  • Klik Send. Hasilnya akan seperti ini:
[
    {
        "id": 1,
        "nama": "Pantai Kuta",
        "deskripsi": "Pantai dengan sunset terkenal",
        "lokasi": "Bali",
        "hargaTiket": 15000,
        "kategoriId": 1,
        "kategori": {
            "id": 1,
            "nama": "Pantai",
            "deskripsi": "Wisata tepi laut dengan pasir putih"
        }
    },
    {
        "id": 2,
        "nama": "Gunung Bromo",
        "deskripsi": "Gunung berapi aktif dengan lautan pasir",
        "lokasi": "Jawa Timur",
        "hargaTiket": 30000,
        "kategoriId": 2,
        "kategori": {
            "id": 2,
            "nama": "Gunung",
            "deskripsi": "Pendakian dan pemandangan alam"
        }
    },
    ...
]

Perhatikan ada objek kategori di dalam setiap item. Keren kan?

2. GET satu wisata

  • Method: GET
  • URL: https://localhost:7000/api/wisata/1
  • Hasil: satu objek wisata dengan kategori.

3. POST tambah wisata baru

  • Method: POST
  • URL: https://localhost:7000/api/wisata
  • Body (raw JSON):
    {
        "nama": "Raja Ampat",
        "deskripsi": "Surga bawah laut",
        "lokasi": "Papua Barat",
        "hargaTiket": 50000,
        "kategoriId": 1
    }
  • Klik Send. Response 201 Created dengan data baru (id akan terisi otomatis).

4. PUT ubah wisata

  • Method: PUT
  • URL: https://localhost:7000/api/wisata/4 (id yang baru ditambahkan)
  • Body:
    {
        "id": 4,
        "nama": "Raja Ampat",
        "deskripsi": "Surga bawah laut dengan keanekaragaman hayati",
        "lokasi": "Papua Barat",
        "hargaTiket": 55000,
        "kategoriId": 1
    }
  • Klik Send. Response 204 No Content. Cek dengan GET.

5. DELETE hapus wisata

  • Method: DELETE
  • URL: https://localhost:7000/api/wisata/4
  • Klik Send. Response 204 No Content. Cek GET lagi, data sudah hilang.
💡 Coba juga kasih KategoriId yang tidak ada (misal 999). Harusnya dapat response 400 Bad Request dengan pesan error. Ini tandanya validasi kita bekerja!

Membersihkan Data yang Tidak Perlu (Opsional)

Mungkin kamu perhatikan, di response GET, selain kategori, tidak ada data lain yang aneh. Tapi bagaimana kalau kita tidak ingin menampilkan semua field kategori? Atau kita ingin menyembunyikan sesuatu? Nanti kita bisa pakai DTO (Data Transfer Object). Tapi untuk sekarang, biarkan dulu, yang penting fungsional.

Apa yang Terjadi Kalau Kita Hapus Kategori yang Masih Dipakai?

Coba hapus kategori yang masih punya wisata (misal kategori Pantai yang masih dipakai Raja Ampat). Di Postman, DELETE ke /api/kategori/1. Pasti error 500 karena database melarang. Itu karena foreign key mencegah penghapusan. Nanti kita bisa atur agar otomatis menghapus semua wisata terkait (cascade delete) atau melarang. Untuk sekarang, kita biarkan saja sebagai pelajaran: jangan hapus kategori yang masih punya anak.

Selamat! Sekarang Kamu Bisa CRUD Dua Tabel Berelasi

API kamu sekarang sudah bisa mengelola tempat wisata lengkap dengan kategorinya. Ini adalah fondasi untuk aplikasi wisata yang lebih besar. Nanti kita akan menambahkan fitur login, upload gambar, dan lainnya.

Di artikel berikutnya (ASP.NET Core API #7), kita akan menambahkan fitur Login dan Registrasi dengan JWT. API kita akan mulai diamankan sehingga tidak semua orang bisa sembarangan nambah, ubah, atau hapus data. Seru kan?

😎 Kalau ada error, jangan langsung stres. Coba debug dengan teliti, siapa tahu cuma salah ketik. Ingat, programmer hebat lahir dari banyak error!

Ditulis oleh Kakak programmer yang dulu juga suka liburan ke pantai. Kalau ada pertanyaan, tulis di komentar ya!

Lebih baru Lebih lama

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