Penggunaan API Eksternal - Perwira Learning Center

Latar Belakang

Pada pembahasan kali ini, saya mempelajari penggunaan REST API dari layanan eksternal dalam pengembangan aplikasi menggunakan Express.js. Jika sebelumnya saya membuat REST API sendiri, maka pada tahap ini saya mencoba memahami bagaimana cara memanfaatkan API yang sudah disediakan oleh pihak ketiga untuk mengambil dan menampilkan data. Hal ini penting karena dalam pengembangan aplikasi modern, integrasi dengan layanan eksternal merupakan kebutuhan yang umum dilakukan.

Mempelajari penggunaan REST API luar membantu saya memahami bagaimana proses request dan response bekerja dalam konteks yang lebih luas, tidak hanya pada server yang saya bangun sendiri. Selain itu, saya juga belajar bagaimana membaca dokumentasi API, memahami endpoint yang tersedia, serta menyesuaikan kebutuhan aplikasi dengan data yang disediakan oleh layanan tersebut.

Alat dan Bahan

A. Perangkat Lunak

  1. Terminal : digunakan untuk membuat folder proyek serta menginstal dependensi yang dibutuhkan.
  2. Visual Studio Code : digunakan sebagai text editor untuk menuliskan dan mengelola kode program.
  3. Node.js : runtime environment agar JavaScript dapat dijalankan di sisi server.
  4. NPM (Node Package Manager) : digunakan untuk mengelola dan menginstal library atau dependensi yang diperlukan.
  5. Postman : digunakan untuk melakukan pengujian REST API.
  6. Web Browser (Google Chrome) : digunakan untuk mengakses aplikasi melalui URL yang tersedia.

B. Perangkat Keras

  1. Laptop dengan sistem operasi Ubuntu.

Apa Itu Spoonacular?

Dalam pengembangan website modern, penggunaan rest api eksternal menjadi salah satu solusi efisien untuk memperoleh data tanpa harus membangun sistem dari nol. Pada proyek ini, saya memilih menggunakan spoonacular sebagai sumber data resep makanan.

Spoonacular adalah layanan rest api yang menyediakan berbagai informasi terkait dunia kuliner, seperti resep makanan, bahan masakan, informasi nutrisi, hingga meal planning. Spoonacular memungkinkan developer untuk mengakses ribuan data resep secara real-time melalui endpoint berbasis http.

Dibandingkan membuat database resep secara manual, penggunaan spoonacular jauh lebih efisien karena data sudah tersedia dan dapat langsung digunakan. Hal ini memungkinkan fokus pengembangan lebih diarahkan pada logika aplikasi dan tampilan antarmuka, bukan pada pengumpulan data.

Cara Mendapatkan API KEY Spoonacular

Untuk dapat menggunakan spoonacular, diperlukan api key sebagai bentuk autentikasi. Berikut langkah-langkahnya:

  1. buka website resmi spoonacular di
    Spoonacular

  2. klik menu sign up dan lakukan pendaftaran akun

  3. setelah berhasil login, masuk ke dashboard

  4. pada bagian profile atau api console, akan tersedia api key

  5. salin api key tersebut untuk digunakan pada proyek

API KEY ini nantinya akan dikirim bersama request agar server spoonacular mengenali aplikasi kita.

Struktur Projek

recipe-app/
├── backend/
│   └── package.json
├── frontend/
│   └── index.html
│   └── Recipe.js
│   └── style.css
├── .env

Instalasi Dependency

install dependency yang dibutuhkan:

npm init -y npm install express axios dotenv npm install nodemon --save-dev
  • express → framework backend

  • axios → untuk melakukan http request ke spoonacular

  • dotenv → menyimpan api key dengan aman

Konfigurasi API KEY (.env)

  1. Buat file .env
  2. Masukkan API KEY ke dalam file .env
    API_KEY=masukkan_api_key_kamu
  3. di file Recipe.js tambahkan:
    require('dotenv').config()

Ini supaya api key tidak ditulis langsung di dalam kode agar keamaan tetap terjamin. Apabila API terpublikasi maka API bisa digunakan oleh orang lain, hal ini bisa berdampak ke keamaan aplikasi dan juga berkemungkinan terjadi lonjakan tagihan penggunaan API.

Code

index.html

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="UTF-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>Recipe Finder</title>

    <link rel="preconnect" href="https://fonts.googleapis.com" />

    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

    <link

      href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap"

      rel="stylesheet"

    />

      <link

      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"

      rel="stylesheet"

    />

    <link rel="stylesheet" href="style.css">

  </head>

  <body>

    <div class="recipe-finder-container">

      <h1>Recipe Finder</h1>


      <form id="search-form" class="row g-2 justify-content-center mb-3">

        <div class="col-md-6">

          <input

            type="text"

            id="search-input"

            class="form-control"

            placeholder="Search recipe (e.g. pasta)"

          />

        </div>

        <div class="col-auto">

          <button type="submit" class="btn btn-primary">

            Search

          </button>

        </div>

      </form>


      <div class="text-center mb-4">

        <button id="random-button" class="btn btn-outline-success">

          Get Random Recipe

        </button>

      </div>

      <div class="message" id="message-area">

        Search for a recipe or get a random one!

      </div>


      <div class="results-grid" id="results-grid"></div>

    </div>


    <div class="modal-container hidden" id="recipe-modal">

      <div class="modal-content">

        <button id="modal-close-btn" class="modal-close-btn">+</button>

        <div id="recipe-details-content"></div>

      </div>

    </div>

    <script src="Recipe.js"></script>

  </body>

</html>

Recipe.js

const API_KEY = "YOUR_API_KEY";
const SEARCH_API_URL = "https://api.spoonacular.com/recipes/complexSearch";
const RANDOM_API_URL = "https://api.spoonacular.com/recipes/random";
const LOOKUP_API_URL = "https://api.spoonacular.com/recipes/";
const searchForm = document.getElementById("search-form");
const searchInput = document.getElementById("search-input");
const resultsGrid = document.getElementById("results-grid");
const messageArea = document.getElementById("message-area");
const randomButton = document.getElementById("random-button");
const modal = document.getElementById("recipe-modal");
const modalContent = document.getElementById("recipe-details-content");
const modalCloseBtn = document.getElementById("modal-close-btn");
searchForm.addEventListener("submit", (e) => {
  e.preventDefault();
  const searchTerm = searchInput.value.trim();
  if (!searchTerm) {
    showMessage("please enter a search term", true);
    return;
  }
  searchRecipes(searchTerm);
});
async function searchRecipes(query) {
  showMessage(`searching for "${query}"...`, false, true);
  resultsGrid.innerHTML = "";
  try {
    const response = await fetch(
      `${SEARCH_API_URL}?query=${encodeURIComponent(query)}&apiKey=${API_KEY}`
    );
    if (!response.ok) throw new Error("network error");
    const data = await response.json();
    clearMessage();
    if (data.results && data.results.length > 0) {
      displayRecipes(data.results);
    } else {
      showMessage(`no recipes found for "${query}"`);
    }
  } catch (error) {
    showMessage("something went wrong. please try again.", true);
  }
}
randomButton.addEventListener("click", getRandomRecipe);
async function getRandomRecipe() {
  showMessage("fetching random recipe...", false, true);
  resultsGrid.innerHTML = "";
  try {
    const response = await fetch(
      `${RANDOM_API_URL}?number=1&apiKey=${API_KEY}`
    );
    if (!response.ok) throw new Error("random fetch error");
    const data = await response.json();
    clearMessage();
    if (data.recipes && data.recipes.length > 0) {
      displayRecipes(data.recipes);
    } else {
      showMessage("could not fetch random recipe", true);
    }
  } catch (error) {
    showMessage("failed to fetch random recipe", true);
  }
}
function displayRecipes(recipes) {
  if (!recipes || recipes.length === 0) {
    showMessage("no recipes to display");
    return;
  }
  recipes.forEach((recipe) => {
    const recipeDiv = document.createElement("div");
    recipeDiv.classList.add("recipe-item");
    recipeDiv.dataset.id = recipe.id;
    recipeDiv.innerHTML = `
      <img src="${recipe.image}" alt="${recipe.title}" loading="lazy">
      <h3>${recipe.title}</h3>
    `;
    resultsGrid.appendChild(recipeDiv);
  });
}
resultsGrid.addEventListener("click", (e) => {
  const card = e.target.closest(".recipe-item");
  if (!card) return;
  const recipeId = card.dataset.id;
  getRecipeDetails(recipeId);
});
async function getRecipeDetails(id) {
  showModal();
  try {
    const response = await fetch(
      `${LOOKUP_API_URL}${id}/information?apiKey=${API_KEY}`
    );
    if (!response.ok) throw new Error("detail fetch error");
    const recipe = await response.json();
    displayRecipeDetails(recipe);
  } catch (error) {
    modalContent.innerHTML =
      '<p class="message error">failed to load recipe details</p>';
  }
}
function displayRecipeDetails(recipe) {
  const ingredientsHTML = recipe.extendedIngredients
    ? `
      <h3>ingredients</h3>
      <ul>
        ${recipe.extendedIngredients
          .map((ing) => `<li>${ing.original}</li>`)
          .join("")}
      </ul>
    `
    : "";
  const instructionsHTML = `
    <h3>instructions</h3>
    <div>${recipe.instructions || "instructions not available"}</div>
  `;
  modalContent.innerHTML = `
    <h2>${recipe.title}</h2>
    <img src="${recipe.image}" alt="${recipe.title}">
    <h3>ready in ${recipe.readyInMinutes} minutes</h3>
    ${ingredientsHTML}
    ${instructionsHTML}
  `;
}
function showModal() {
  modal.classList.remove("hidden");
  document.body.style.overflow = "hidden";
}
function closeModal() {
  modal.classList.add("hidden");
  document.body.style.overflow = "";
}
modalCloseBtn.addEventListener("click", closeModal);
modal.addEventListener("click", (e) => {
  if (e.target === modal) closeModal();
});
function showMessage(message, isError = false) {
  messageArea.textContent = message;
  messageArea.className = "message";
  if (isError) messageArea.classList.add("error");
}
function clearMessage() {
  messageArea.textContent = "";
  messageArea.className = "message";
}

style.css

:root {
  --light-bg: #f4f6f8;
  --text-dark: #2d3436;
  --primary-color: #43a047;
  --primary-dark: #388e3c;
  --primary-light: #e8f5e9;
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.07);
  --shadow-md: 0 6px 20px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 12px 35px rgba(0, 0, 0, 0.13);
  --radius: 12px;
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "Poppins", sans-serif;
  line-height: 1.7;
  padding: 40px 15px;
  background-color: var(--light-bg);
  background-image: radial-gradient(circle at 20% 20%, rgba(67, 160, 71, 0.05) 0%, transparent 50%),
                    radial-gradient(circle at 80% 80%, rgba(67, 160, 71, 0.04) 0%, transparent 50%);
  color: var(--text-dark);
}
.recipe-finder-container {
  max-width: 1200px;
  padding: 0 15px;
  text-align: center;
  margin-inline: auto;
}
h1 {
  font-size: 2.5rem;
  font-weight: 700;
  margin-bottom: 30px;
  letter-spacing: -1px;
  background: linear-gradient(135deg, var(--primary-color), #1b5e20);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.results-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 25px;
  margin-top: 30px;
}
.recipe-item img {
  width: 100%;
  height: 220px;
  object-fit: cover;
  border-radius: var(--radius) var(--radius) 0 0;
  transition: transform 0.35s ease;
}
.recipe-item {
  background-color: #fff;
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  text-align: left;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  width: 100%;
  overflow: hidden;
  transition: all 0.28s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  border: 1px solid rgba(0, 0, 0, 0.04);
}
.recipe-item:hover {
  transform: translateY(-7px);
  box-shadow: var(--shadow-lg);
}
.recipe-item:hover img {
  transform: scale(1.04);
}
.recipe-item h3 {
  font-size: 1.05rem;
  font-weight: 600;
  padding: 14px 16px;
  margin: 0;
  color: var(--text-dark);
  line-height: 1.4;
  border-top: 2px solid var(--primary-light);
}
.modal-container {
  position: fixed;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 1;
  visibility: visible;
  backdrop-filter: blur(4px);
  padding: 20px;
  transition: opacity 0.3s ease, visibility 0s ease 0s;
}
.modal-container.hidden {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0s ease 0.3s,
    backdrop-filter 0.3s ease;
  backdrop-filter: blur(0px);
}
.modal-content {
  background-color: #fff;
  max-width: 800px;
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  border-radius: var(--radius);
  padding: 40px 45px;
  position: relative;
  transform: scale(1);
  transition: transform 0.3s ease;
  scrollbar-width: thin;
  scrollbar-color: #dee2e6 transparent;
}
.modal-content::-webkit-scrollbar {
  width: 5px;
}
.modal-content::-webkit-scrollbar-thumb {
  background-color: #ced4da;
  border-radius: 99px;
}
.modal-container.hidden .modal-content {
  transform: scale(0.96);
}
.modal-content .modal-close-btn {
  position: absolute;
  top: 15px;
  right: 15px;
  background-color: var(--light-bg);
  border: 1px solid #e2e6ea;
  border-radius: 50%;
  height: 36px;
  width: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  cursor: pointer;
  color: #868e96;
  transform: rotate(45deg);
  transition: all 0.22s ease;
  line-height: 1;
}
.modal-content .modal-close-btn:hover {
  transform: rotate(135deg);
  color: var(--text-dark);
  background-color: #e2e6ea;
  border-color: #ced4da;
}
#recipe-details-content h2 {
  font-size: 1.75rem;
  font-weight: 700;
  margin-bottom: 18px;
  color: var(--primary-color);
  line-height: 1.3;
}
#recipe-details-content img {
  width: 100%;
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}
#recipe-details-content h3 {
  font-size: 1.1rem;
  font-weight: 600;
  margin-top: 28px;
  margin-bottom: 10px;
  border-bottom: 2px solid var(--primary-light);
  padding-bottom: 8px;
  color: var(--text-dark);
  display: flex;
  align-items: center;
  gap: 6px;
}
#recipe-details-content h3::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 1em;
  background: var(--primary-color);
  border-radius: 2px;
}
#recipe-details-content ul {
  list-style: none;
  padding-left: 0;
  margin-bottom: 25px;
}
#recipe-details-content ul li {
  padding: 8px 4px;
  border-bottom: 1px dashed #e9ecef;
  font-size: 0.95rem;
  display: flex;
  align-items: center;
  gap: 8px;
}
#recipe-details-content ul li::before {
  content: '▸';
  color: var(--primary-color);
  font-size: 0.8rem;
  flex-shrink: 0;
}
#recipe-details-content ul li:last-child {
  border-bottom: none;
}
#recipe-details-content p {
  line-height: 1.8;
  font-size: 1rem;
  color: #495057;
}
#recipe-details-content a {
  color: var(--primary-color);
  text-decoration: none;
  font-weight: 600;
  border-bottom: 1.5px solid transparent;
  transition: border-color 0.2s ease;
}
#recipe-details-content a:hover {
  color: var(--primary-dark);
  border-bottom-color: var(--primary-dark);
}
#recipe-details-content .video-wrapper {
  margin-top: 15px;
  margin-bottom: 8px;
  border-radius: var(--radius);
  overflow: hidden;
}
.message {
  margin: 25px auto;
  padding: 12px 16px;
  max-width: 600px;
  font-size: 0.95rem;
  border-radius: var(--radius);
}
.message.error {
  color: #c0392b;
  background-color: #fdecea;
  border: 1px solid #f5c2c2;
}
.message.info,
.message.loading {
  color: #0e7490;
  background-color: #e0f2fe;
  border: 1px solid #bae6fd;
}

Dokumentasi Website

1. Tampilan Awal


2. Pencarian

3. Hasil Pencarian

4. Detail Resep

5. Resep Random



Melalui praktik penggunaan REST API luar, saya memahami cara mengirim request ke endpoint tertentu menggunakan HTTP method yang sesuai, kemudian mengolah response yang diterima dalam format JSON. Saya juga mempelajari bagaimana menangani kemungkinan error, seperti kegagalan koneksi atau response yang tidak sesuai, agar aplikasi tetap berjalan dengan baik.

Selain itu, saya memperoleh pemahaman bahwa integrasi API eksternal memerlukan perhatian terhadap struktur data yang diterima, autentikasi (jika diperlukan), serta pengelolaan data sebelum ditampilkan ke pengguna. Proses ini memberikan gambaran nyata mengenai bagaimana aplikasi dapat saling terhubung dan bertukar data secara efisien.

Kesimpulan

Penggunaan REST API dari layanan eksternal memberikan pengalaman belajar yang lebih luas dalam pengembangan aplikasi berbasis server. Tidak hanya memahami cara membangun API, tetapi juga memahami bagaimana memanfaatkan API yang sudah ada untuk mendukung fitur aplikasi.

Dengan memahami cara mengintegrasikan REST API luar, saya memiliki dasar yang lebih kuat dalam membangun aplikasi yang terhubung dengan berbagai layanan, serta siap menghadapi kebutuhan pengembangan perangkat lunak yang lebih kompleks di masa mendatang.

Daftar Pustaka

Spoonacular. (2024). Food API Documentation. Diakses pada 13 Februari 2026, dari https://spoonacular.com/food-api

Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures (Disertasi Doktoral). University of California, Irvine.

Node.js. (2024). Node.js Documentation. Diakses pada 13 Februari 2026, dari https://nodejs.org

Express.js. (2024). Express.js Documentation. Diakses pada 13 Februari 2026, dari https://expressjs.com

Axios. (2024). Axios Documentation. Diakses pada 13 Februari 2026, dari https://axios-http.com