Pengolahan Data Request (Params, Query, Body) di Express.js - Pewira Learning Center


1. Latar Belakang

Hari keenam pembelajaran Express.js di Perwira Learning Center membahas Pengolahan Data Request. Ini ibarat menerima pesanan di restoran:

  • Params = Nomor meja (tetap, wajib ada)
  • Query = Pilihan tambahan (pedas/kurang pedas, es/kurang es)
  • Body = Menu yang dipesan (nasi goreng, ayam bakar, dll)

Tanpa memahami cara mengambil data dari client, server kita seperti pelayan yang tuli: tidak tahu apa yang mau dipesan customer!

2. Alat dan Bahan

a. Perangkat Lunak

  1. Express.js Project yang sudah berjalan
  2. Postman - Untuk testing berbagai jenis request
  3. VS Code - Untuk coding
  4. Browser - Untuk testing GET requests
  5. Node.js - Runtime environment

b. Perangkat Keras

  1. Laptop/PC standar

3. Pembahasan

3.1 Tiga Jenis Data dari Client

Analogi Restoran:

javascript
// Contoh request ke restoran:
GET /meja/5/pesanan?pedas=true&minuman=es-teh

Body: {
  "makanan": "nasi goreng seafood",
  "jumlah": 2,
  "catatan": "tidak pakai telur"
}

// Breakdown:
// Params: /meja/5/pesanan    → Meja nomor 5
// Query: ?pedas=true&minuman=es-teh → Pilihan tambahan
// Body: { makanan, jumlah, catatan } → Pesanan utama

3.2 Perbedaan Params, Query, dan Body

FiturParamsQueryBody
LokasiURL pathURL setelah ?Request body
Contoh/users/123?page=2&limit=10{name: "Andi"}
HTTP MethodSemuaGET terutamaPOST, PUT, PATCH
Wajib/OpsionalWajib (bagian dari route)OpsionalOpsional (tapi sering wajib)
UkuranTerbatasTerbatasBisa besar
Use CaseIdentifikasi resourceFilter, sort, paginationData utama untuk create/update

3.3 Praktik Lengkap: Sistem Pendaftaran Event

Buat file data-processing.js:

javascript
const express = require('express');
const app = express();
const PORT = 3004;

// Middleware UNTUK PARSE DATA
app.use(express.json()); // Untuk JSON body
app.use(express.urlencoded({ extended: true })); // Untuk form data

// ==================== DATABASE SIMULASI ====================
let events = [
  {
    id: 1,
    title: "Workshop JavaScript Dasar",
    category: "workshop",
    speaker: "Budi Santoso",
    date: "2024-03-15",
    time: "09:00",
    duration: 3,
    capacity: 50,
    registered: 35,
    price: 150000,
    location: "Gedung A Lt. 3",
    tags: ["javascript", "pemula", "coding"],
    isActive: true
  },
  {
    id: 2,
    title: "Seminar AI untuk Bisnis",
    category: "seminar",
    speaker: "Dr. Sari Wijaya",
    date: "2024-03-20",
    time: "13:00",
    duration: 2,
    capacity: 100,
    registered: 85,
    price: 0,
    location: "Auditorium Utama",
    tags: ["ai", "bisnis", "teknologi"],
    isActive: true
  },
  {
    id: 3,
    title: "Bootcamp Full-Stack Web Dev",
    category: "bootcamp",
    speaker: "Team DevHub",
    date: "2024-04-01",
    time: "10:00",
    duration: 40,
    capacity: 30,
    registered: 28,
    price: 2500000,
    location: "Online",
    tags: ["web", "fullstack", "react", "nodejs"],
    isActive: true
  }
];

let registrations = [];
let nextEventId = 4;
let nextRegId = 1;

// ==================== HOME ROUTE ====================
app.get('/', (req, res) => {
  res.json({
    message: "🎯 Event Registration System API",
    description: "Demo pengolahan data request: Params, Query, dan Body",
    endpoints: {
      events: {
        getAll: "GET    /api/events",
        getById: "GET    /api/events/:id",
        search: "GET    /api/events/search?q=...",
        filter: "GET    /api/events/filter?category=...&price=...",
        create: "POST   /api/events",
        update: "PUT    /api/events/:id",
        delete: "DELETE /api/events/:id",
        register: "POST   /api/events/:eventId/register"
      },
      registrations: {
        getAll: "GET    /api/registrations",
        getByEvent: "GET    /api/events/:eventId/registrations",
        getById: "GET    /api/registrations/:regId"
      }
    },
    dataTypes: {
      params: "URL segment (/:id)",
      query: "URL query (?key=value)",
      body: "Request body (JSON/Form)"
    }
  });
});

// ==================== 1. PARAMETERS (ROUTE PARAMS) ====================

// 🔍 GET EVENT BY ID (Parameter: id)
app.get('/api/events/:id', (req, res) => {
  console.log("📌 ROUTE PARAMS:", req.params);
  
  const eventId = parseInt(req.params.id);
  
  // Validasi: apakah id number?
  if (isNaN(eventId)) {
    return res.status(400).json({
      success: false,
      error: "Invalid Parameter",
      message: "Event ID harus berupa angka",
      received: req.params.id
    });
  }
  
  const event = events.find(e => e.id === eventId);
  
  if (!event) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Event dengan ID ${eventId} tidak ditemukan`
    });
  }
  
  res.json({
    success: true,
    message: `Event ID ${eventId} ditemukan`,
    data: event
  });
});

// 👥 GET REGISTRATIONS FOR SPECIFIC EVENT (Multiple params)
app.get('/api/events/:eventId/registrations/:status?', (req, res) => {
  console.log("📌 MULTIPLE PARAMS:", req.params);
  
  const eventId = parseInt(req.params.eventId);
  const status = req.params.status; // Optional parameter
  
  // Validasi event
  const event = events.find(e => e.id === eventId);
  if (!event) {
    return res.status(404).json({
      success: false,
      error: "Event Not Found",
      message: `Event dengan ID ${eventId} tidak ditemukan`
    });
  }
  
  // Filter registrations
  let eventRegistrations = registrations.filter(r => r.eventId === eventId);
  
  if (status) {
    eventRegistrations = eventRegistrations.filter(r => r.status === status);
  }
  
  res.json({
    success: true,
    message: `Registrasi untuk Event "${event.title}"${status ? ` (status: ${status})` : ''}`,
    event: {
      id: event.id,
      title: event.title,
      registered: event.registered,
      capacity: event.capacity
    },
    count: eventRegistrations.length,
    data: eventRegistrations
  });
});

// ==================== 2. QUERY PARAMETERS ====================

// 🔎 SEARCH EVENTS (Query: q)
app.get('/api/events/search', (req, res) => {
  console.log("🔍 QUERY PARAMS:", req.query);
  
  const searchQuery = req.query.q?.toLowerCase() || '';
  
  if (!searchQuery) {
    return res.status(400).json({
      success: false,
      error: "Missing Query Parameter",
      message: "Query parameter 'q' diperlukan untuk pencarian",
      example: "/api/events/search?q=javascript"
    });
  }
  
  const results = events.filter(event => 
    event.title.toLowerCase().includes(searchQuery) ||
    event.speaker.toLowerCase().includes(searchQuery) ||
    event.tags.some(tag => tag.toLowerCase().includes(searchQuery)) ||
    event.category.toLowerCase().includes(searchQuery)
  );
  
  res.json({
    success: true,
    message: `Hasil pencarian untuk "${searchQuery}"`,
    query: searchQuery,
    count: results.length,
    data: results
  });
});

// 🎯 FILTER EVENTS (Multiple query parameters)
app.get('/api/events/filter', (req, res) => {
  console.log("🎯 MULTIPLE QUERY PARAMS:", req.query);
  
  const { 
    category, 
    minPrice, 
    maxPrice, 
    dateFrom, 
    dateTo,
    sortBy = 'date',
    order = 'asc',
    page = 1,
    limit = 10
  } = req.query;
  
  // Start with all events
  let filteredEvents = [...events];
  
  // 1. Filter by category
  if (category) {
    filteredEvents = filteredEvents.filter(e => 
      e.category.toLowerCase() === category.toLowerCase()
    );
  }
  
  // 2. Filter by price range
  if (minPrice || maxPrice) {
    const min = parseFloat(minPrice) || 0;
    const max = parseFloat(maxPrice) || Infinity;
    
    filteredEvents = filteredEvents.filter(e => 
      e.price >= min && e.price <= max
    );
  }
  
  // 3. Filter by date range
  if (dateFrom || dateTo) {
    const fromDate = dateFrom ? new Date(dateFrom) : new Date('1900-01-01');
    const toDate = dateTo ? new Date(dateTo) : new Date('2100-12-31');
    
    filteredEvents = filteredEvents.filter(e => {
      const eventDate = new Date(e.date);
      return eventDate >= fromDate && eventDate <= toDate;
    });
  }
  
  // 4. Filter active only (default)
  filteredEvents = filteredEvents.filter(e => e.isActive);
  
  // 5. Sorting
  filteredEvents.sort((a, b) => {
    if (sortBy === 'date') {
      return order === 'asc' 
        ? new Date(a.date) - new Date(b.date)
        : new Date(b.date) - new Date(a.date);
    }
    if (sortBy === 'price') {
      return order === 'asc' ? a.price - b.price : b.price - a.price;
    }
    if (sortBy === 'title') {
      return order === 'asc' 
        ? a.title.localeCompare(b.title)
        : b.title.localeCompare(a.title);
    }
    return 0;
  });
  
  // 6. Pagination
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const startIndex = (pageNum - 1) * limitNum;
  const endIndex = pageNum * limitNum;
  
  const paginatedEvents = filteredEvents.slice(startIndex, endIndex);
  const totalPages = Math.ceil(filteredEvents.length / limitNum);
  
  res.json({
    success: true,
    message: "Events filtered successfully",
    filters: {
      category,
      priceRange: { min: minPrice, max: maxPrice },
      dateRange: { from: dateFrom, to: dateTo },
      sort: { by: sortBy, order }
    },
    pagination: {
      currentPage: pageNum,
      totalPages,
      totalItems: filteredEvents.length,
      itemsPerPage: limitNum,
      hasNextPage: endIndex < filteredEvents.length,
      hasPrevPage: startIndex > 0
    },
    count: paginatedEvents.length,
    data: paginatedEvents
  });
});

// 📊 GET ALL EVENTS WITH QUERY OPTIONS
app.get('/api/events', (req, res) => {
  console.log("📊 QUERY FOR ALL EVENTS:", req.query);
  
  const { 
    category, 
    freeOnly, 
    upcomingOnly,
    sort = 'date:asc'
  } = req.query;
  
  let filteredEvents = [...events];
  
  // Filter by category
  if (category) {
    filteredEvents = filteredEvents.filter(e => 
      e.category.toLowerCase() === category.toLowerCase()
    );
  }
  
  // Filter free events only
  if (freeOnly === 'true') {
    filteredEvents = filteredEvents.filter(e => e.price === 0);
  }
  
  // Filter upcoming events
  if (upcomingOnly === 'true') {
    const today = new Date();
    filteredEvents = filteredEvents.filter(e => new Date(e.date) >= today);
  }
  
  // Filter active only
  filteredEvents = filteredEvents.filter(e => e.isActive);
  
  // Sorting
  const [sortField, sortOrder] = sort.split(':');
  filteredEvents.sort((a, b) => {
    let aVal, bVal;
    
    if (sortField === 'date') {
      aVal = new Date(a.date);
      bVal = new Date(b.date);
    } else if (sortField === 'price') {
      aVal = a.price;
      bVal = b.price;
    } else if (sortField === 'title') {
      aVal = a.title;
      bVal = b.title;
    } else {
      aVal = a[sortField];
      bVal = b[sortField];
    }
    
    if (sortOrder === 'desc') {
      return aVal < bVal ? 1 : -1;
    }
    return aVal > bVal ? 1 : -1;
  });
  
  res.json({
    success: true,
    message: "Events retrieved successfully",
    filters: { category, freeOnly, upcomingOnly, sort },
    count: filteredEvents.length,
    data: filteredEvents
  });
});

// ==================== 3. REQUEST BODY ====================

// ➕ CREATE NEW EVENT (POST dengan Body)
app.post('/api/events', (req, res) => {
  console.log("📦 REQUEST BODY:", req.body);
  console.log("📦 REQUEST HEADERS:", req.headers['content-type']);
  
  // Validasi: data harus ada
  if (!req.body || Object.keys(req.body).length === 0) {
    return res.status(400).json({
      success: false,
      error: "Empty Request Body",
      message: "Request body tidak boleh kosong",
      requiredFields: ["title", "category", "speaker", "date"]
    });
  }
  
  const { 
    title, 
    category, 
    speaker, 
    date, 
    time = "10:00",
    duration = 2,
    capacity = 50,
    price = 0,
    location = "TBD",
    tags = []
  } = req.body;
  
  // Validasi required fields
  const missingFields = [];
  if (!title) missingFields.push("title");
  if (!category) missingFields.push("category");
  if (!speaker) missingFields.push("speaker");
  if (!date) missingFields.push("date");
  
  if (missingFields.length > 0) {
    return res.status(400).json({
      success: false,
      error: "Missing Required Fields",
      message: "Field berikut wajib diisi",
      missing: missingFields
    });
  }
  
  // Validasi data types
  const errors = [];
  
  if (typeof title !== 'string') errors.push("title harus string");
  if (typeof capacity !== 'number') errors.push("capacity harus number");
  if (typeof price !== 'number') errors.push("price harus number");
  if (!Array.isArray(tags)) errors.push("tags harus array");
  
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: "Invalid Data Types",
      message: "Tipe data tidak valid",
      errors: errors
    });
  }
  
  // Create new event
  const newEvent = {
    id: nextEventId++,
    title,
    category: category.toLowerCase(),
    speaker,
    date,
    time,
    duration: parseInt(duration),
    capacity: parseInt(capacity),
    registered: 0,
    price: parseFloat(price),
    location,
    tags: Array.isArray(tags) ? tags : [tags],
    isActive: true,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  
  events.push(newEvent);
  
  // Response dengan data yang diterima dan diproses
  res.status(201).json({
    success: true,
    message: "Event berhasil dibuat",
    receivedData: req.body, // Data asli dari client
    processedData: newEvent, // Data setelah diproses
    location: `/api/events/${newEvent.id}`
  });
});

// ✏️ UPDATE EVENT (PUT dengan Body + Params)
app.put('/api/events/:id', (req, res) => {
  console.log("📦 PUT REQUEST:");
  console.log("  Params:", req.params);
  console.log("  Body:", req.body);
  
  const eventId = parseInt(req.params.id);
  const updateData = req.body;
  
  // Validasi event exists
  const eventIndex = events.findIndex(e => e.id === eventId);
  if (eventIndex === -1) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Event dengan ID ${eventId} tidak ditemukan`
    });
  }
  
  // Validasi body tidak kosong
  if (!updateData || Object.keys(updateData).length === 0) {
    return res.status(400).json({
      success: false,
      error: "Empty Update Data",
      message: "Data update tidak boleh kosong",
      currentData: events[eventIndex]
    });
  }
  
  // Update event (full update dengan PUT)
  events[eventIndex] = {
    ...events[eventIndex],
    ...updateData,
    updatedAt: new Date().toISOString()
  };
  
  res.json({
    success: true,
    message: `Event ID ${eventId} berhasil diupdate`,
    updatedFields: Object.keys(updateData),
    data: events[eventIndex]
  });
});

// 📝 REGISTER FOR EVENT (Params + Body)
app.post('/api/events/:eventId/register', (req, res) => {
  const eventId = parseInt(req.params.eventId);
  const registrationData = req.body;
  
  console.log("🎫 REGISTRATION DATA:");
  console.log("  Event ID (params):", eventId);
  console.log("  Registration Data (body):", registrationData);
  
  // 1. Cek event exists dan masih ada kuota
  const event = events.find(e => e.id === eventId);
  if (!event) {
    return res.status(404).json({
      success: false,
      error: "Event Not Found",
      message: `Event dengan ID ${eventId} tidak ditemukan`
    });
  }
  
  if (!event.isActive) {
    return res.status(400).json({
      success: false,
      error: "Event Inactive",
      message: `Event "${event.title}" sudah tidak aktif`
    });
  }
  
  if (event.registered >= event.capacity) {
    return res.status(400).json({
      success: false,
      error: "Event Full",
      message: `Event "${event.title}" sudah penuh`,
      capacity: event.capacity,
      registered: event.registered
    });
  }
  
  // 2. Validasi data pendaftaran
  const { name, email, phone } = registrationData;
  
  if (!name || !email) {
    return res.status(400).json({
      success: false,
      error: "Missing Required Fields",
      message: "Nama dan email wajib diisi",
      received: registrationData
    });
  }
  
  // 3. Cek apakah email sudah terdaftar di event ini
  const alreadyRegistered = registrations.some(
    r => r.eventId === eventId && r.email === email
  );
  
  if (alreadyRegistered) {
    return res.status(409).json({
      success: false,
      error: "Already Registered",
      message: `Email ${email} sudah terdaftar untuk event ini`
    });
  }
  
  // 4. Buat registrasi
  const newRegistration = {
    id: nextRegId++,
    eventId,
    name,
    email,
    phone: phone || "",
    registrationDate: new Date().toISOString(),
    status: "pending",
    paymentStatus: event.price > 0 ? "unpaid" : "free",
    ticketNumber: `TICK-${eventId}-${nextRegId.toString().padStart(4, '0')}`
  };
  
  registrations.push(newRegistration);
  
  // 5. Update jumlah pendaftar di event
  event.registered += 1;
  
  res.status(201).json({
    success: true,
    message: `Berhasil mendaftar untuk "${event.title}"`,
    registration: newRegistration,
    event: {
      id: event.id,
      title: event.title,
      date: event.date,
      time: event.time,
      location: event.location,
      registered: event.registered,
      capacity: event.capacity
    },
    summary: {
      totalRegistrations: registrations.length,
      eventRegistrations: registrations.filter(r => r.eventId === eventId).length,
      remainingSlots: event.capacity - event.registered
    }
  });
});

// ==================== 4. FORM DATA vs JSON ====================

// 📝 HANDLE FORM SUBMISSION (application/x-www-form-urlencoded)
app.post('/api/contact', (req, res) => {
  console.log("📝 FORM DATA RECEIVED:", req.body);
  console.log("📝 CONTENT TYPE:", req.headers['content-type']);
  
  const { name, email, message, subject = "General Inquiry" } = req.body;
  
  // Validasi
  if (!name || !email || !message) {
    return res.status(400).json({
      success: false,
      error: "Incomplete Form",
      message: "Semua field wajib diisi",
      received: req.body
    });
  }
  
  // Simulasikan pengiriman email
  res.json({
    success: true,
    message: "Pesan berhasil dikirim",
    data: {
      from: `${name} <${email}>`,
      subject,
      message,
      receivedAt: new Date().toISOString(),
      referenceId: `CONTACT-${Date.now()}`
    }
  });
});

// 📤 UPLOAD SIMULATION (multipart/form-data)
// Note: Untuk upload file sebenarnya butuh middleware seperti multer
app.post('/api/upload', (req, res) => {
  // Untuk form-data, kita perlu middleware khusus
  // Tapi kita bisa simulasi menerima metadata
  
  res.json({
    success: true,
    message: "Endpoint upload (simulasi)",
    note: "Untuk upload file, gunakan middleware seperti multer",
    example: {
      headers: {
        "Content-Type": "multipart/form-data"
      },
      body: {
        "file": "[binary data]",
        "description": "text field"
      }
    }
  });
});

// ==================== 5. HEADERS & VALIDASI ====================

// 🔐 API KEY VALIDATION (Headers)
app.post('/api/secure/events', (req, res) => {
  console.log("🔐 REQUEST HEADERS:", req.headers);
  
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey) {
    return res.status(401).json({
      success: false,
      error: "Unauthorized",
      message: "API key diperlukan",
      headerRequired: "x-api-key"
    });
  }
  
  // Validasi API key (simulasi)
  const validApiKeys = ["secret-key-123", "admin-key-456"];
  if (!validApiKeys.includes(apiKey)) {
    return res.status(403).json({
      success: false,
      error: "Forbidden",
      message: "API key tidak valid"
    });
  }
  
  // Jika valid, proses request
  const { title, category } = req.body;
  
  res.json({
    success: true,
    message: "Event created with API key authentication",
    data: { title, category },
    auth: {
      apiKey: "***" + apiKey.slice(-4), // Hanya tampilkan 4 karakter terakhir
      timestamp: new Date().toISOString()
    }
  });
});

// ==================== 6. COMBINED EXAMPLE ====================

// 🎯 COMPLEX SEARCH WITH ALL DATA TYPES
app.get('/api/search/advanced', (req, res) => {
  console.log("🎯 ADVANCED SEARCH:");
  console.log("  Query:", req.query);
  console.log("  Headers:", req.headers);
  
  const { 
    q, // query string
    category,
    minPrice,
    maxPrice,
    date,
    sort = "relevance:desc",
    page = 1,
    limit = 20
  } = req.query;
  
  // Juga bisa ambil dari headers
  const userAgent = req.headers['user-agent'];
  const acceptLanguage = req.headers['accept-language'];
  
  let results = [...events];
  
  // Filtering logic (sama seperti sebelumnya)
  if (q) {
    const query = q.toLowerCase();
    results = results.filter(e => 
      e.title.toLowerCase().includes(query) ||
      e.speaker.toLowerCase().includes(query) ||
      e.tags.some(tag => tag.toLowerCase().includes(query))
    );
  }
  
  if (category) {
    results = results.filter(e => e.category === category);
  }
  
  // Pagination
  const pageNum = parseInt(page);
  const limitNum = parseInt(limit);
  const startIndex = (pageNum - 1) * limitNum;
  const endIndex = pageNum * limitNum;
  
  const paginatedResults = results.slice(startIndex, endIndex);
  
  res.json({
    success: true,
    message: "Advanced search results",
    metadata: {
      searchParams: req.query,
      clientInfo: {
        userAgent: userAgent?.substring(0, 50) + '...',
        language: acceptLanguage
      },
      pagination: {
        page: pageNum,
        limit: limitNum,
        totalResults: results.length,
        totalPages: Math.ceil(results.length / limitNum)
      }
    },
    count: paginatedResults.length,
    data: paginatedResults
  });
});

// ==================== ERROR HANDLING ====================

// 404 Handler
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    error: "Route Not Found",
    message: `Cannot ${req.method} ${req.originalUrl}`,
    receivedData: {
      params: req.params,
      query: req.query,
      body: req.body ? "Body exists (hidden for security)" : "No body"
    }
  });
});

// Error handler untuk data processing errors
app.use((err, req, res, next) => {
  console.error('🔥 Data Processing Error:', err);
  
  // Cek jika error dari JSON parsing
  if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
    return res.status(400).json({
      success: false,
      error: "Invalid JSON",
      message: "Request body mengandung JSON yang tidak valid",
      tip: "Pastikan format JSON benar (gunakan double quotes)"
    });
  }
  
  res.status(500).json({
    success: false,
    error: "Data Processing Error",
    message: "Terjadi kesalahan dalam memproses data request"
  });
});

// ==================== START SERVER ====================
app.listen(PORT, () => {
  console.log("=".repeat(60));
  console.log("📥 DATA PROCESSING PRACTICE SERVER");
  console.log("=".repeat(60));
  console.log(`🌐 Server: http://localhost:${PORT}`);
  console.log(`📅 Events: ${events.length} events available`);
  console.log(`📝 Registrations: ${registrations.length} total`);
  console.log("");
  console.log("🎯 CONTOH REQUEST UNTUK DICOBA:");
  console.log("");
  console.log("1. PARAMS (Route Parameters):");
  console.log("   GET    /api/events/1");
  console.log("   GET    /api/events/2/registrations/confirmed");
  console.log("");
  console.log("2. QUERY (Query Parameters):");
  console.log("   GET    /api/events/search?q=javascript");
  console.log("   GET    /api/events/filter?category=workshop&minPrice=0&maxPrice=200000");
  console.log("   GET    /api/events?freeOnly=true&sort=date:desc");
  console.log("");
  console.log("3. BODY (Request Body - gunakan Postman):");
  console.log("   POST   /api/events");
  console.log("   Body: {");
  console.log('     "title": "Belajar Node.js",');
  console.log('     "category": "workshop",');
  console.log('     "speaker": "John Doe",');
  console.log('     "date": "2024-04-10"');
  console.log("   }");
  console.log("");
  console.log("4. KOMBINASI:");
  console.log("   POST   /api/events/1/register");
  console.log("   Body: {");
  console.log('     "name": "Andi",');
  console.log('     "email": "andi@email.com",');
  console.log('     "phone": "08123456789"');
  console.log("   }");
  console.log("");
  console.log("5. HEADERS:");
  console.log("   POST   /api/secure/events");
  console.log('   Headers: x-api-key: "secret-key-123"');
  console.log("=".repeat(60));
});

3.4 Cara Testing dengan Postman

Test 1: Route Parameters

text
GET http://localhost:3004/api/events/1
GET http://localhost:3004/api/events/abc  ← Error: bukan angka

Test 2: Query Parameters

text
GET http://localhost:3004/api/events/search?q=javascript
GET http://localhost:3004/api/events/filter?category=workshop&minPrice=0&maxPrice=100000&sort=price:asc&page=1&limit=5
GET http://localhost:3004/api/events?freeOnly=true&upcomingOnly=true

Test 3: Request Body (JSON)

text
POST http://localhost:3004/api/events
Headers: Content-Type: application/json
Body (raw JSON):
{
  "title": "Workshop REST API",
  "category": "workshop",
  "speaker": "Jane Smith",
  "date": "2024-04-15",
  "capacity": 40,
  "price": 200000,
  "tags": ["api", "rest", "express"]
}

Test 4: Form Data

text
POST http://localhost:3004/api/contact
Headers: Content-Type: application/x-www-form-urlencoded
Body (form-data):
name: Budi Santoso
email: budi@email.com
message: Saya ingin bertanya tentang workshop
subject: Inquiry

3.5 Validasi Data yang Lebih Advanced

javascript
// Middleware untuk validasi
const validateEventData = (req, res, next) => {
  const { title, date, capacity, price } = req.body;
  const errors = [];
  
  // Validasi title
  if (!title || title.length < 5) {
    errors.push("Title minimal 5 karakter");
  }
  
  // Validasi date
  if (date) {
    const eventDate = new Date(date);
    const today = new Date();
    if (eventDate < today) {
      errors.push("Tanggal event tidak boleh di masa lalu");
    }
  }
  
  // Validasi capacity
  if (capacity && (capacity < 1 || capacity > 1000)) {
    errors.push("Kapasitas harus antara 1-1000");
  }
  
  // Validasi price
  if (price && price < 0) {
    errors.push("Harga tidak boleh negatif");
  }
  
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: "Validation Failed",
      errors: errors
    });
  }
  
  next();
};

// Gunakan middleware
app.post('/api/events', validateEventData, (req, res) => {
  // Handler hanya dijalankan jika validasi lolos
});

3.6 Menggunakan req.params, req.query, req.body Bersamaan

javascript
// Contoh: Update review untuk product tertentu dengan filter
app.put('/api/products/:productId/reviews/:reviewId', (req, res) => {
  const { productId, reviewId } = req.params; // dari URL
  const { rating, comment } = req.body;       // dari request body
  const { adminKey } = req.query;             // dari query string
  
  console.log({
    params: { productId, reviewId },
    body: { rating, comment },
    query: { adminKey }
  });
  
  // Logika bisnis menggunakan semua data
  // ...
});

3.7 File Upload (Pengenalan)

javascript
// Instal multer dulu: npm install multer
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

// Handle file upload
app.post('/api/upload/photo', upload.single('photo'), (req, res) => {
  console.log("File:", req.file);      // Info file
  console.log("Body:", req.body);      // Text fields lainnya
  
  res.json({
    success: true,
    message: "File uploaded successfully",
    file: req.file,
    otherData: req.body
  });
});

3.8 Best Practices

1. Always Validate Input

javascript
// ❌ Tidak aman
app.post('/api/users', (req, res) => {
  const user = req.body;
  // langsung save ke database ← DANGER!
});

// ✅ Validasi dulu
app.post('/api/users', (req, res) => {
  const { name, email, age } = req.body;
  
  // Validasi
  if (!name || name.length < 2) {
    return res.status(400).json({ error: "Name invalid" });
  }
  
  if (!email || !email.includes('@')) {
    return res.status(400).json({ error: "Email invalid" });
  }
  
  // Baru proses
});

2. Sanitize Data

javascript
// Hapus whitespace berlebih
const cleanName = name.trim();

// Convert ke lowercase untuk konsistensi
const cleanEmail = email.toLowerCase().trim();

// Parse angka dengan aman
const cleanAge = parseInt(age) || 0;

3. Use Default Values

javascript
app.get('/api/products', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const sort = req.query.sort || 'name:asc';
  
  // Lebih aman daripada langsung pakai req.query
});

3.9 Common Mistakes

Mistake 1: Not Parsing Numbers

javascript
// ❌ Wrong
const id = req.params.id; // "123" (string)
const price = req.body.price; // "150000" (string)

// ✅ Correct
const id = parseInt(req.params.id);
const price = parseFloat(req.body.price);

Mistake 2: Assuming Data Exists

javascript
// ❌ Wrong (akan error jika query tidak ada)
const search = req.query.search.toLowerCase();

// ✅ Correct
const search = req.query.search?.toLowerCase() || '';

Mistake 3: Not Setting Content-Type

javascript
// Di Postman, jangan lupa set:
// Content-Type: application/json
// Kalau tidak, req.body akan undefined

3.10 Latihan Praktikum

Exercise 1: User Profile API

javascript
// Buat endpoint untuk:
// 1. GET /api/users/:userId/profile
// 2. PUT /api/users/:userId/profile
//    - Body: { name, bio, avatarUrl, socialLinks }
//    - Query: ?validate=true (untuk validasi ekstra)
// 3. GET /api/users/search
//    - Query: ?name=...&location=...&skills=... (array)

Exercise 2: Order System

javascript
// Buat endpoint untuk order makanan:
// POST /api/restaurants/:restId/orders
// Body: {
//   items: [{ id, quantity, notes }],
//   deliveryAddress: { ... },
//   paymentMethod: "cash/card",
//   specialInstructions: "..."
// }
// Query: ?priority=true (untuk express delivery)

4. Daftar Pustaka

  1. Express.js Documentation (2024). Request Object. Diakses dari https://expressjs.com/en/api.html#req
  2. MDN Web Docs (2024). HTTP Request Methods. Diakses dari https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
  3. OWASP Security Guidelines (2024). Input Validation Cheat Sheet. Diakses dari https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
  4. Postman Learning Center (2024). Sending Requests Tutorial. Diakses dari https://learning.postman.com/docs/sending-requests/requests/
  5. Perwira Learning Center (2024). Modul Data Processing & Validation. Materi internal PLC.
  6. JavaScript.info (2024). Fetch API and Forms. Diakses dari https://javascript.info/fetch
  7. Multer Documentation (2024). File Upload Middleware. Diakses dari https://github.com/expressjs/multer

 

Posting Komentar

0 Komentar