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
- Express.js Project yang sudah ada
- Postman - Untuk testing REST API
- VS Code - Untuk coding
- Browser - Untuk testing GET requests
- Terminal - Untuk menjalankan server
b. Perangkat Keras
- 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:
- Client-Server - Pemisahan jelas antara client dan server
- Stateless - Setiap request independen, server tidak simpan state client
- Cacheable - Response bisa di-cache
- Uniform Interface - Interface konsisten untuk semua resource
- Layered System - Client tidak perlu tahu struktur internal server
- 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: CREATE → POST /api/resources READ → GET /api/resources READ → GET /api/resources/:id UPDATE → PUT /api/resources/:id (full update) UPDATE → PATCH /api/resources/:id (partial update) DELETE → DELETE /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 StatisticsTest 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
- Postman - API testing and documentation
- Swagger/OpenAPI - API documentation standard
- Insomnia - Alternative to Postman
- JSON Server - Mock REST API quickly
- Express Generator - Scaffold Express apps
4. Daftar Pustaka
- Santri Koding (2026). Tutorial Express.js Resful API. https://santrikoding.com
- IDN.id (n.d.). Cara membuat API dengan Node.Js dan Express Untuk Pemula. https://www.idn.id
- Medium (2018). Struktur Aplikasi Express JS. https://medium.com/@gustialfianmp
- Anak Teknik (2025). Membuat RESTful API dengan Express.js. https://www.anakteknik.co.id
- Postman API Network (2024). Public REST APIs for Practice. Diakses dari https://www.postman.com/explore
- MDN Web Docs (2024). REST - Beginner's Guide. Diakses dari https://developer.mozilla.org/en-US/docs/Glossary/REST

0 Komentar