REST API dan Struktur Endpoint di Express.js - Perwira Learning Center

 

1. Latar Belakang

    Hari ini kita masih belajar Express.js dan membahas REST API dan Struktur Endpoint. REST API itu ibarat menu restoran yang terorganisir:

  • Restoran = Aplikasi kita
  • Menu = Kumpulan endpoint yang tersedia
  • Kategori Menu = Pengelompokan endpoint (users, products, orders)
  • Deskripsi Menu = Dokumentasi setiap endpoint

Tanpa REST API yang baik, client kita akan bingung: "Mau pesan nasi goreng harus ke mana? URL-nya apa? Kirim data apa saja?"

2. Alat dan Bahan

a. Perangkat Lunak

  1. Express.js Project yang sudah ada
  2. Postman - Untuk testing REST API
  3. VS Code - Untuk coding
  4. Browser - Untuk testing GET requests
  5. Terminal - Untuk menjalankan server

b. Perangkat Keras

  1. Laptop/PC dengan spesifikasi standar

3. Pembahasan

3.1 Apa itu REST API?

REST (Representational State Transfer) = Arsitektur untuk membuat web service yang menggunakan HTTP.

6 Prinsip RESTful API:

  1. Client-Server - Pemisahan jelas antara client dan server
  2. Stateless - Setiap request independen, server tidak simpan state client
  3. Cacheable - Response bisa di-cache
  4. Uniform Interface - Interface konsisten untuk semua resource
  5. Layered System - Client tidak perlu tahu struktur internal server
  6. Code on Demand (opsional) - Server bisa kirim kode executable ke client

Analogi SIMPLE:

text
REST API = Kantor Pelayanan Publik

Resource (Resources) = Layanan yang tersedia (KTP, SIM, Paspor)
Endpoint (Endpoints) = Loket pelayanan (Loket 1: KTP, Loket 2: SIM)
HTTP Methods = Jenis pelayanan (Ambil data, Ajukan baru, Perpanjang, Batalkan)

3.2 Komponen REST API

javascript
// Contoh REST API endpoint untuk "books"
GET    /api/books          // Ambil semua buku
GET    /api/books/123      // Ambil buku dengan ID 123
POST   /api/books          // Buat buku baru
PUT    /api/books/123      // Update buku 123 (full update)
PATCH  /api/books/123      // Update sebagian buku 123
DELETE /api/books/123      // Hapus buku 123

3.3 Praktik Lengkap: REST API untuk Perpustakaan Digital

Buat file rest-api-practice.js:

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

// Middleware
app.use(express.json());

// Helper untuk logging
const logger = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
  next();
};
app.use(logger);

// ==================== DATABASE SIMULASI ====================
let books = [
  {
    id: 1,
    title: "JavaScript: The Good Parts",
    author: "Douglas Crockford",
    isbn: "978-0596517748",
    category: "Programming",
    year: 2008,
    pages: 176,
    available: true,
    borrowedBy: null,
    createdAt: "2024-01-15T10:30:00Z",
    updatedAt: "2024-01-15T10:30:00Z"
  },
  {
    id: 2,
    title: "Clean Code: A Handbook of Agile Software Craftsmanship",
    author: "Robert C. Martin",
    isbn: "978-0132350884",
    category: "Programming",
    year: 2008,
    pages: 464,
    available: false,
    borrowedBy: "user123",
    createdAt: "2024-01-20T14:15:00Z",
    updatedAt: "2024-02-01T09:45:00Z"
  },
  {
    id: 3,
    title: "The Pragmatic Programmer",
    author: "David Thomas, Andrew Hunt",
    isbn: "978-0201616224",
    category: "Programming",
    year: 1999,
    pages: 352,
    available: true,
    borrowedBy: null,
    createdAt: "2024-02-01T08:00:00Z",
    updatedAt: "2024-02-01T08:00:00Z"
  }
];

let members = [
  {
    id: 1,
    memberId: "MEM001",
    name: "Andi Wijaya",
    email: "andi@email.com",
    phone: "081234567890",
    membershipType: "Premium",
    joinDate: "2024-01-01",
    totalBorrowed: 5,
    isActive: true
  },
  {
    id: 2,
    memberId: "MEM002",
    name: "Budi Santoso",
    email: "budi@email.com",
    phone: "082345678901",
    membershipType: "Regular",
    joinDate: "2024-01-15",
    totalBorrowed: 2,
    isActive: true
  }
];

let nextBookId = 4;
let nextMemberId = 3;

// ==================== HOME ROUTE (API DOCS) ====================
app.get('/', (req, res) => {
  res.json({
    name: "📚 Digital Library REST API",
    version: "1.0.0",
    description: "RESTful API untuk sistem perpustakaan digital",
    documentation: {
      baseURL: `http://localhost:${PORT}`,
      endpoints: {
        books: {
          collection: "GET    /api/books",
          single: "GET    /api/books/:id",
          create: "POST   /api/books",
          update: "PUT    /api/books/:id",
          partialUpdate: "PATCH  /api/books/:id",
          delete: "DELETE /api/books/:id",
          search: "GET    /api/books/search?query=...",
          byCategory: "GET    /api/books/category/:category",
          available: "GET    /api/books/available"
        },
        members: {
          collection: "GET    /api/members",
          single: "GET    /api/members/:id",
          create: "POST   /api/members",
          update: "PUT    /api/members/:id",
          delete: "DELETE /api/members/:id",
          borrowBook: "POST   /api/members/:memberId/borrow/:bookId",
          returnBook: "POST   /api/members/:memberId/return/:bookId",
          borrowedBooks: "GET    /api/members/:memberId/books"
        }
      }
    },
    status: {
      totalBooks: books.length,
      totalMembers: members.length,
      availableBooks: books.filter(b => b.available).length
    }
  });
});

// ==================== BOOKS RESOURCE ====================

// 📖 GET ALL BOOKS (Collection)
app.get('/api/books', (req, res) => {
  // Pagination parameters
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const startIndex = (page - 1) * limit;
  const endIndex = page * limit;

  // Filtering
  let filteredBooks = [...books];
  
  if (req.query.category) {
    filteredBooks = filteredBooks.filter(b => 
      b.category.toLowerCase() === req.query.category.toLowerCase()
    );
  }
  
  if (req.query.available === 'true') {
    filteredBooks = filteredBooks.filter(b => b.available);
  }
  
  if (req.query.available === 'false') {
    filteredBooks = filteredBooks.filter(b => !b.available);
  }

  // Sorting
  if (req.query.sort) {
    const [field, order] = req.query.sort.split(':');
    filteredBooks.sort((a, b) => {
      if (a[field] < b[field]) return order === 'asc' ? -1 : 1;
      if (a[field] > b[field]) return order === 'asc' ? 1 : -1;
      return 0;
    });
  }

  // Pagination result
  const paginatedBooks = filteredBooks.slice(startIndex, endIndex);
  
  // Response metadata
  const totalPages = Math.ceil(filteredBooks.length / limit);

  res.status(200).json({
    success: true,
    message: "Books retrieved successfully",
    data: paginatedBooks,
    pagination: {
      currentPage: page,
      totalPages: totalPages,
      totalItems: filteredBooks.length,
      itemsPerPage: limit,
      hasNextPage: endIndex < filteredBooks.length,
      hasPrevPage: startIndex > 0
    },
    filters: {
      category: req.query.category,
      available: req.query.available,
      sort: req.query.sort
    }
  });
});

// 🔍 GET SINGLE BOOK (Resource)
app.get('/api/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const book = books.find(b => b.id === bookId);

  if (!book) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Book with ID ${bookId} not found`
    });
  }

  res.status(200).json({
    success: true,
    message: "Book retrieved successfully",
    data: book
  });
});

// 🔎 SEARCH BOOKS
app.get('/api/books/search', (req, res) => {
  const query = req.query.q?.toLowerCase() || '';
  
  if (!query) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: "Search query parameter 'q' is required"
    });
  }

  const results = books.filter(book =>
    book.title.toLowerCase().includes(query) ||
    book.author.toLowerCase().includes(query) ||
    book.category.toLowerCase().includes(query) ||
    book.isbn.includes(query)
  );

  res.status(200).json({
    success: true,
    message: `Search results for "${query}"`,
    query: query,
    count: results.length,
    data: results
  });
});

// 📚 GET BOOKS BY CATEGORY
app.get('/api/books/category/:category', (req, res) => {
  const category = req.params.category.toLowerCase();
  const categoryBooks = books.filter(b => 
    b.category.toLowerCase() === category
  );

  res.status(200).json({
    success: true,
    message: `Books in category "${category}"`,
    category: category,
    count: categoryBooks.length,
    data: categoryBooks
  });
});

// ✅ GET AVAILABLE BOOKS
app.get('/api/books/available', (req, res) => {
  const availableBooks = books.filter(b => b.available);

  res.status(200).json({
    success: true,
    message: "Available books retrieved",
    count: availableBooks.length,
    data: availableBooks
  });
});

// ➕ CREATE NEW BOOK (Resource Creation)
app.post('/api/books', (req, res) => {
  const { title, author, isbn, category, year, pages } = req.body;

  // Validation
  if (!title || !author || !isbn) {
    return res.status(400).json({
      success: false,
      error: "Validation Error",
      message: "Title, author, and ISBN are required fields",
      required: ["title", "author", "isbn"]
    });
  }

  // Check if ISBN already exists
  const isbnExists = books.some(b => b.isbn === isbn);
  if (isbnExists) {
    return res.status(409).json({
      success: false,
      error: "Conflict",
      message: `Book with ISBN ${isbn} already exists`
    });
  }

  // Create new book
  const newBook = {
    id: nextBookId++,
    title,
    author,
    isbn,
    category: category || "Uncategorized",
    year: year || new Date().getFullYear(),
    pages: pages || 0,
    available: true,
    borrowedBy: null,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };

  books.push(newBook);

  // RESTful response: 201 Created with Location header
  res.status(201)
    .set('Location', `/api/books/${newBook.id}`)
    .json({
      success: true,
      message: "Book created successfully",
      data: newBook
    });
});

// ✏️ UPDATE BOOK (Full Update - PUT)
app.put('/api/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === bookId);

  if (bookIndex === -1) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Book with ID ${bookId} not found`
    });
  }

  const { title, author, isbn, category, year, pages, available } = req.body;

  // Validation for required fields in PUT
  if (!title || !author || !isbn) {
    return res.status(400).json({
      success: false,
      error: "Validation Error",
      message: "Title, author, and ISBN are required for full update"
    });
  }

  // Full replacement
  books[bookIndex] = {
    ...books[bookIndex],
    title,
    author,
    isbn,
    category: category || books[bookIndex].category,
    year: year || books[bookIndex].year,
    pages: pages || books[bookIndex].pages,
    available: available !== undefined ? available : books[bookIndex].available,
    updatedAt: new Date().toISOString()
  };

  res.status(200).json({
    success: true,
    message: "Book updated successfully",
    data: books[bookIndex]
  });
});

// 🔄 PARTIAL UPDATE BOOK (PATCH)
app.patch('/api/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === bookId);

  if (bookIndex === -1) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Book with ID ${bookId} not found`
    });
  }

  // Only update provided fields
  const updates = req.body;
  const allowedFields = ['title', 'author', 'category', 'year', 'pages', 'available', 'borrowedBy'];
  
  Object.keys(updates).forEach(key => {
    if (allowedFields.includes(key)) {
      books[bookIndex][key] = updates[key];
    }
  });

  books[bookIndex].updatedAt = new Date().toISOString();

  res.status(200).json({
    success: true,
    message: "Book partially updated successfully",
    data: books[bookIndex],
    updatedFields: Object.keys(updates).filter(k => allowedFields.includes(k))
  });
});

// 🗑️ DELETE BOOK (Resource Deletion)
app.delete('/api/books/:id', (req, res) => {
  const bookId = parseInt(req.params.id);
  const bookIndex = books.findIndex(b => b.id === bookId);

  if (bookIndex === -1) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Book with ID ${bookId} not found`
    });
  }

  // Check if book is currently borrowed
  if (!books[bookIndex].available) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: `Cannot delete book that is currently borrowed`
    });
  }

  const deletedBook = books.splice(bookIndex, 1)[0];

  res.status(200).json({
    success: true,
    message: "Book deleted successfully",
    data: deletedBook,
    remainingBooks: books.length
  });
});

// ==================== MEMBERS RESOURCE ====================

// 👥 GET ALL MEMBERS
app.get('/api/members', (req, res) => {
  res.status(200).json({
    success: true,
    message: "Members retrieved successfully",
    count: members.length,
    data: members
  });
});

// 👤 GET SINGLE MEMBER
app.get('/api/members/:id', (req, res) => {
  const memberId = parseInt(req.params.id);
  const member = members.find(m => m.id === memberId);

  if (!member) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Member with ID ${memberId} not found`
    });
  }

  res.status(200).json({
    success: true,
    message: "Member retrieved successfully",
    data: member
  });
});

// ➕ CREATE NEW MEMBER
app.post('/api/members', (req, res) => {
  const { name, email, phone, membershipType } = req.body;

  // Validation
  if (!name || !email) {
    return res.status(400).json({
      success: false,
      error: "Validation Error",
      message: "Name and email are required"
    });
  }

  // Check email uniqueness
  const emailExists = members.some(m => m.email === email);
  if (emailExists) {
    return res.status(409).json({
      success: false,
      error: "Conflict",
      message: `Member with email ${email} already exists`
    });
  }

  // Generate member ID
  const lastMember = members[members.length - 1];
  const lastNumber = lastMember ? parseInt(lastMember.memberId.replace('MEM', '')) : 0;
  const newMemberId = `MEM${String(lastNumber + 1).padStart(3, '0')}`;

  const newMember = {
    id: nextMemberId++,
    memberId: newMemberId,
    name,
    email,
    phone: phone || "",
    membershipType: membershipType || "Regular",
    joinDate: new Date().toISOString().split('T')[0],
    totalBorrowed: 0,
    isActive: true
  };

  members.push(newMember);

  res.status(201).json({
    success: true,
    message: "Member created successfully",
    data: newMember
  });
});

// ==================== NESTED RESOURCES ====================

// 📥 BORROW BOOK (Nested action)
app.post('/api/members/:memberId/borrow/:bookId', (req, res) => {
  const memberId = parseInt(req.params.memberId);
  const bookId = parseInt(req.params.bookId);

  const member = members.find(m => m.id === memberId);
  const book = books.find(b => b.id === bookId);

  if (!member) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Member with ID ${memberId} not found`
    });
  }

  if (!book) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Book with ID ${bookId} not found`
    });
  }

  if (!book.available) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: `Book "${book.title}" is not available for borrowing`
    });
  }

  if (!member.isActive) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: `Member "${member.name}" is not active`
    });
  }

  // Update book status
  book.available = false;
  book.borrowedBy = member.memberId;
  book.updatedAt = new Date().toISOString();

  // Update member stats
  member.totalBorrowed += 1;

  res.status(200).json({
    success: true,
    message: `Book "${book.title}" borrowed by ${member.name}`,
    data: {
      book: {
        id: book.id,
        title: book.title,
        borrowedBy: member.name,
        borrowedDate: new Date().toISOString()
      },
      member: {
        id: member.id,
        name: member.name,
        totalBorrowed: member.totalBorrowed
      }
    }
  });
});

// 📤 RETURN BOOK
app.post('/api/members/:memberId/return/:bookId', (req, res) => {
  const memberId = parseInt(req.params.memberId);
  const bookId = parseInt(req.params.bookId);

  const member = members.find(m => m.id === memberId);
  const book = books.find(b => b.id === bookId);

  if (!member || !book) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: member ? `Book not found` : `Member not found`
    });
  }

  if (book.available) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: `Book "${book.title}" is not currently borrowed`
    });
  }

  if (book.borrowedBy !== member.memberId) {
    return res.status(400).json({
      success: false,
      error: "Bad Request",
      message: `Book "${book.title}" is not borrowed by ${member.name}`
    });
  }

  // Update book status
  book.available = true;
  book.borrowedBy = null;
  book.updatedAt = new Date().toISOString();

  res.status(200).json({
    success: true,
    message: `Book "${book.title}" returned successfully`,
    data: {
      book: {
        id: book.id,
        title: book.title,
        available: true
      },
      member: {
        id: member.id,
        name: member.name
      }
    }
  });
});

// 📚 GET MEMBER'S BORROWED BOOKS
app.get('/api/members/:memberId/books', (req, res) => {
  const memberId = parseInt(req.params.memberId);
  const member = members.find(m => m.id === memberId);

  if (!member) {
    return res.status(404).json({
      success: false,
      error: "Not Found",
      message: `Member with ID ${memberId} not found`
    });
  }

  const borrowedBooks = books.filter(b => 
    b.borrowedBy === member.memberId && !b.available
  );

  res.status(200).json({
    success: true,
    message: `Books borrowed by ${member.name}`,
    member: {
      id: member.id,
      name: member.name,
      totalBorrowed: member.totalBorrowed
    },
    count: borrowedBooks.length,
    data: borrowedBooks
  });
});

// ==================== STATISTICS & UTILITY ====================

// 📊 GET LIBRARY STATISTICS
app.get('/api/statistics', (req, res) => {
  const totalBooks = books.length;
  const availableBooks = books.filter(b => b.available).length;
  const borrowedBooks = totalBooks - availableBooks;
  const totalMembers = members.length;
  const activeMembers = members.filter(m => m.isActive).length;

  // Category breakdown
  const categories = {};
  books.forEach(book => {
    categories[book.category] = (categories[book.category] || 0) + 1;
  });

  res.status(200).json({
    success: true,
    message: "Library statistics",
    data: {
      books: {
        total: totalBooks,
        available: availableBooks,
        borrowed: borrowedBooks,
        categories: categories
      },
      members: {
        total: totalMembers,
        active: activeMembers,
        premium: members.filter(m => m.membershipType === 'Premium').length,
        regular: members.filter(m => m.membershipType === 'Regular').length
      },
      popularBooks: books
        .filter(b => !b.available)
        .sort((a, b) => b.borrowedCount - a.borrowedCount)
        .slice(0, 5)
        .map(b => ({ id: b.id, title: b.title, author: b.author }))
    },
    timestamp: new Date().toISOString()
  });
});

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

// 404 Handler
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    error: "Not Found",
    message: `Cannot ${req.method} ${req.originalUrl}`,
    suggestion: "Check available endpoints at GET /"
  });
});

// Error handler
app.use((err, req, res, next) => {
  console.error('🔥 REST API Error:', err);
  res.status(500).json({
    success: false,
    error: "Internal Server Error",
    message: "Something went wrong with the REST API",
    requestId: Date.now() // Simple request ID for tracking
  });
});

// ==================== START SERVER ====================
app.listen(PORT, () => {
  console.log("=".repeat(60));
  console.log("📚 DIGITAL LIBRARY REST API");
  console.log("=".repeat(60));
  console.log(`🌐 Base URL: http://localhost:${PORT}`);
  console.log(`📖 Total Books: ${books.length}`);
  console.log(`👥 Total Members: ${members.length}`);
  console.log("");
  console.log("📡 RESTFUL ENDPOINTS:");
  console.log("");
  console.log("📚 BOOKS RESOURCE:");
  console.log("   GET    /api/books                    → Get all books (with pagination)");
  console.log("   GET    /api/books/:id               → Get book by ID");
  console.log("   GET    /api/books/search?q=...      → Search books");
  console.log("   GET    /api/books/category/:cat     → Books by category");
  console.log("   GET    /api/books/available         → Available books");
  console.log("   POST   /api/books                    → Create new book");
  console.log("   PUT    /api/books/:id               → Full update book");
  console.log("   PATCH  /api/books/:id               → Partial update book");
  console.log("   DELETE /api/books/:id               → Delete book");
  console.log("");
  console.log("👥 MEMBERS RESOURCE:");
  console.log("   GET    /api/members                 → Get all members");
  console.log("   GET    /api/members/:id             → Get member by ID");
  console.log("   POST   /api/members                 → Create new member");
  console.log("   POST   /members/:mid/borrow/:bid    → Borrow book");
  console.log("   POST   /members/:mid/return/:bid    → Return book");
  console.log("   GET    /members/:mid/books          → Member's borrowed books");
  console.log("");
  console.log("📊 UTILITY:");
  console.log("   GET    /api/statistics              → Library statistics");
  console.log("");
  console.log("💡 REST Principles Applied:");
  console.log("   • Resource-based URLs");
  console.log("   • Proper HTTP methods");
  console.log("   • Stateless communication");
  console.log("   • Consistent response format");
  console.log("=".repeat(60));
});

3.4 Best Practices REST API Endpoint Design

1. Resource Naming (Plural Nouns)

javascript
// ✅ Good (consistent, plural)
/api/books
/api/books/123
/api/members
/api/members/456/books

// ❌ Bad (inconsistent)
/api/book
/api/getBook/123
/api/allMembers
/api/member/456/getBooks

2. HTTP Methods yang Tepat

javascript
// CRUD Operations dengan HTTP Methods:
CREATEPOST   /api/resources
READGET    /api/resources
READGET    /api/resources/:id
UPDATEPUT    /api/resources/:id    (full update)
UPDATEPATCH  /api/resources/:id    (partial update)
DELETEDELETE /api/resources/:id

3. Nested Resources untuk Relationship

javascript
// Relationship: Author has many Books
GET    /api/authors/:authorId/books      → Author's books
POST   /api/authors/:authorId/books      → Add book to author
GET    /api/authors/:authorId/books/:bookId → Specific book

// vs Subresource (alternative)
GET    /api/books?author=:authorId       → Filter books by author

4. Filtering, Sorting, Pagination

javascript
// Filtering
GET /api/books?category=programming&available=true

// Sorting
GET /api/books?sort=year:desc&sort=title:asc

// Pagination
GET /api/books?page=2&limit=10

// Search
GET /api/books/search?q=javascript&category=web

5. Response Format Konsisten

javascript
// Success Response
{
  "success": true,
  "message": "Resource retrieved successfully",
  "data": { /* resource data */ },
  "metadata": { /* pagination, filters, etc */ }
}

// Error Response
{
  "success": false,
  "error": "Error Type",
  "message": "Human readable message",
  "details": { /* optional debug info */ }
}

3.5 REST API vs RPC (Remote Procedure Call)

javascript
// ❌ RPC Style (like function calls)
POST /api/createBook
POST /api/updateBook
POST /api/deleteBook
GET  /api/getAllBooks
GET  /api/getBookById

// ✅ REST Style (resource-oriented)
POST   /api/books        // Create
GET    /api/books        // Read all
GET    /api/books/:id    // Read one
PUT    /api/books/:id    // Update
DELETE /api/books/:id    // Delete

3.6 Status Codes yang Tepat untuk REST

javascript
// Success
200 OK                    // GET, PUT, PATCH success
201 Created              // POST success (new resource)
204 No Content           // DELETE success, no body

// Client Errors
400 Bad Request          // Validation error
401 Unauthorized         // Not authenticated
403 Forbidden            // Authenticated but no permission
404 Not Found            // Resource doesn't exist
409 Conflict             // Resource conflict (duplicate)

// Server Errors
500 Internal Server Error
503 Service Unavailable

3.7 Versioning REST API

javascript
// URL Versioning (most common)
/api/v1/books
/api/v2/books

// Header Versioning
GET /api/books
Headers: Accept: application/vnd.myapi.v1+json

// Query Parameter Versioning
GET /api/books?version=1

3.8 Testing REST API dengan Postman

Postman Collection Structure:

text
📁 Library API v1.0
├── 📂 Books
│   ├── 📁 GET Requests
│   │   ├── Get All Books
│   │   ├── Get Book by ID
│   │   ├── Search Books
│   │   └── Get Available Books
│   ├── 📁 POST Requests
│   │   └── Create Book
│   ├── 📁 PUT Requests
│   │   └── Update Book
│   └── 📁 DELETE Requests
│       └── Delete Book
├── 📂 Members
│   ├── Get All Members
│   ├── Create Member
│   └── Borrow/Return Books
└── 📂 Utility
    └── Get Statistics

Test Scenario: Complete Book Lifecycle

javascript
// 1. Create a new book
POST /api/books
{
  "title": "Learning REST API",
  "author": "John Doe",
  "isbn": "123-4567890123",
  "category": "Web Development"
}
// Response: 201 Created with Location: /api/books/4

// 2. Get the created book
GET /api/books/4
// Response: 200 OK with book details

// 3. Update the book
PUT /api/books/4
{
  "title": "Mastering REST API Design",
  "author": "John Doe",
  "isbn": "123-4567890123",
  "category": "API Development",
  "pages": 300
}
// Response: 200 OK

// 4. Partially update
PATCH /api/books/4
{
  "pages": 320
}
// Response: 200 OK

// 5. Delete the book
DELETE /api/books/4
// Response: 200 OK

3.9 HATEOAS (Hypermedia as the Engine of Application State)

Advanced REST: Menambahkan links ke response

javascript
// Response with HATEOAS links
{
  "success": true,
  "data": {
    "id": 1,
    "title": "JavaScript Guide",
    "author": "Jane Doe"
  },
  "_links": {
    "self": { "href": "/api/books/1", "method": "GET" },
    "update": { "href": "/api/books/1", "method": "PUT" },
    "delete": { "href": "/api/books/1", "method": "DELETE" },
    "borrow": { "href": "/api/members/1/borrow/1", "method": "POST" }
  }
}

3.10 Latihan Praktikum

Exercise 1: E-commerce REST API

javascript
// Design REST API untuk toko online:
// Resources: Products, Categories, Orders, Customers, Reviews

// TODO: Design endpoints untuk:
// 1. Products CRUD
// 2. Orders lifecycle (create, update status, cancel)
// 3. Customer orders history
// 4. Product reviews
// 5. Inventory management

Exercise 2: Blog Platform REST API

javascript
// Design REST API untuk platform blog:
// Resources: Users, Posts, Comments, Categories, Tags

// TODO: Implement nested resources:
// GET    /api/users/:userId/posts
// POST   /api/users/:userId/posts
// GET    /api/posts/:postId/comments
// POST   /api/posts/:postId/comments
// GET    /api/categories/:categoryId/posts

3.11 Common REST API Mistakes

Mistake 1: Verbs in URLs

javascript
// ❌ Wrong
GET /api/getBooks
POST /api/createBook
PUT /api/updateBook/1

// ✅ Correct
GET /api/books
POST /api/books
PUT /api/books/1

Mistake 2: Inconsistent Response Format

javascript
// ❌ Inconsistent
{ books: [...] }          // GET /books
{ book: {...} }           // GET /books/1
{ result: "created" }     // POST /books

// ✅ Consistent
{ success: true, data: [...] }
{ success: true, data: {...} }
{ success: true, data: {...}, message: "created" }

Mistake 3: Wrong HTTP Methods

javascript
// ❌ Using GET untuk update
GET /api/books/1?action=delete

// ❌ Using POST untuk get
POST /api/getBooks

// ✅ Correct methods
DELETE /api/books/1
GET /api/books

3.12 Tools untuk REST API Development

  1. Postman - API testing and documentation
  2. Swagger/OpenAPI - API documentation standard
  3. Insomnia - Alternative to Postman
  4. JSON Server - Mock REST API quickly
  5. Express Generator - Scaffold Express apps

4. Daftar Pustaka

  1. Santri Koding (2026). Tutorial Express.js Resful API. https://santrikoding.com
  2. IDN.id (n.d.). Cara membuat API dengan Node.Js dan Express Untuk Pemula. https://www.idn.id
  3. Medium (2018). Struktur Aplikasi Express JS. https://medium.com/@gustialfianmp
  4. Anak Teknik (2025). Membuat RESTful API dengan Express.js. https://www.anakteknik.co.id
  5. Postman API Network (2024). Public REST APIs for Practice. Diakses dari https://www.postman.com/explore
  6. MDN Web Docs (2024). REST - Beginner's Guide. Diakses dari https://developer.mozilla.org/en-US/docs/Glossary/REST

Posting Komentar

0 Komentar