1. Latar Belakang
Sebagai penutup pembelajaran Express.js di Perwira Learning Center, saya akan membuat REST API CRUD sederhana tanpa menggunakan database (hanya dengan data JSON di memory). Project ini bertujuan untuk:
- Melatih pemahaman alur data: Bagaimana data mengalir dari client → server → response
- Mengasah logika algoritma: Implementasi operasi CRUD yang efisien
- Memperkuat struktur Express.js: Penerapan routing, middleware, controllers secara praktis
Ini ibarat simulasi perang akhir - semua ilmu yang dipelajari dari pertemuan 1-9 diaplikasikan dalam satu project nyata!
2. Alat dan Bahan
a. Perangkat Lunak
- Express.js - Framework utama
- Node.js & npm - Runtime dan package manager
- VS Code - Code editor
- Postman - API testing tool
- Git - Version control system
b. Perangkat Keras
- Laptop/PC standar
c. Dependencies
npm install express dotenv cors npm install -D nodemon
3. Pembahasan
3.1 Project Overview: Student Management API
Kita akan membuat API Manajemen Mahasiswa dengan fitur:
📊 FITUR UTAMA: 1. CREATE - Menambah data mahasiswa baru 2. READ - Melihat semua/data spesifik mahasiswa 3. UPDATE - Memperbarui data mahasiswa 4. DELETE - Menghapus data mahasiswa 5. FILTER - Filter berdasarkan jurusan/angkatan 6. SEARCH - Cari berdasarkan nama 7. PAGINATION - Data per halaman
3.2 Struktur Project Sederhana
Untuk project mini ini, kita gunakan struktur yang sederhana namun terstruktur:
student-api/ ├── src/ │ ├── server.js # Entry point │ ├── app.js # Express configuration │ ├── routes/ # API routes │ │ └── students.js # Student endpoints │ ├── controllers/ # Business logic │ │ └── studentController.js │ ├── models/ # Data models │ │ └── Student.js │ └── data/ # Mock data │ └── students.json ├── package.json ├── .env ├── .gitignore └── README.md
3.3 Implementasi Lengkap Project
Step 1: Setup Project
# 1. Buat project folder mkdir student-api && cd student-api # 2. Inisialisasi project npm init -y # 3. Install dependencies npm install express dotenv cors npm install -D nodemon # 4. Buat struktur folder mkdir -p src/{routes,controllers,models,data}
Step 2: File Konfigurasi
package.json
{ "name": "student-api", "version": "1.0.0", "description": "Student Management REST API", "main": "src/server.js", "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "rest-api", "crud", "students"], "author": "Perwira Learning Center", "license": "MIT", "dependencies": { "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2" }, "devDependencies": { "nodemon": "^3.0.1" } }
.env
PORT=3007 NODE_ENV=development API_VERSION=v1
.gitignore
node_modules/ .env .DS_Store *.log
Step 3: Data Model
src/data/students.json
[ { "id": 1, "nim": "20230001", "name": "Ahmad Fauzi", "email": "ahmad@example.com", "major": "Informatika", "year": 2023, "gpa": 3.75, "createdAt": "2024-01-15T10:00:00.000Z", "updatedAt": "2024-01-15T10:00:00.000Z" }, { "id": 2, "nim": "20230002", "name": "Siti Aminah", "email": "siti@example.com", "major": "Sistem Informasi", "year": 2023, "gpa": 3.90, "createdAt": "2024-01-14T09:00:00.000Z", "updatedAt": "2024-01-14T09:00:00.000Z" }, { "id": 3, "nim": "20220001", "name": "Budi Santoso", "email": "budi@example.com", "major": "Teknik Elektro", "year": 2022, "gpa": 3.50, "createdAt": "2024-01-13T08:00:00.000Z", "updatedAt": "2024-01-13T08:00:00.000Z" } ]
src/models/Student.js
const fs = require('fs').promises; const path = require('path'); const dataPath = path.join(__dirname, '../data/students.json'); class Student { // Get all students static async getAll() { try { const data = await fs.readFile(dataPath, 'utf8'); return JSON.parse(data); } catch (error) { throw new Error('Failed to read students data'); } } // Get student by ID static async getById(id) { const students = await this.getAll(); return students.find(student => student.id === parseInt(id)); } // Create new student static async create(studentData) { const students = await this.getAll(); const newId = students.length > 0 ? Math.max(...students.map(s => s.id)) + 1 : 1; const newStudent = { id: newId, ...studentData, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; students.push(newStudent); await this.saveAll(students); return newStudent; } // Update student static async update(id, updateData) { const students = await this.getAll(); const index = students.findIndex(s => s.id === parseInt(id)); if (index === -1) return null; students[index] = { ...students[index], ...updateData, updatedAt: new Date().toISOString() }; await this.saveAll(students); return students[index]; } // Delete student static async delete(id) { const students = await this.getAll(); const index = students.findIndex(s => s.id === parseInt(id)); if (index === -1) return false; students.splice(index, 1); await this.saveAll(students); return true; } // Filter by major static async filterByMajor(major) { const students = await this.getAll(); return students.filter(student => student.major.toLowerCase() === major.toLowerCase() ); } // Search by name static async searchByName(keyword) { const students = await this.getAll(); const keywordLower = keyword.toLowerCase(); return students.filter(student => student.name.toLowerCase().includes(keywordLower) ); } // Get by year static async getByYear(year) { const students = await this.getAll(); return students.filter(student => student.year === parseInt(year)); } // Save all students to file static async saveAll(students) { try { await fs.writeFile(dataPath, JSON.stringify(students, null, 2), 'utf8'); } catch (error) { throw new Error('Failed to save students data'); } } } module.exports = Student;
Step 4: Controller
src/controllers/studentController.js
const Student = require('../models/Student'); // Response formatter const apiResponse = (success, message, data = null, pagination = null) => { const response = { success, message, timestamp: new Date().toISOString() }; if (data !== null) response.data = data; if (pagination !== null) response.pagination = pagination; return response; }; // Validation helper const validateStudent = (data, isUpdate = false) => { const errors = []; if (!isUpdate || data.name !== undefined) { if (!data.name || data.name.trim() === '') { errors.push('Nama tidak boleh kosong'); } if (data.name && data.name.length > 100) { errors.push('Nama maksimal 100 karakter'); } } if (!isUpdate || data.nim !== undefined) { if (!data.nim || data.nim.trim() === '') { errors.push('NIM tidak boleh kosong'); } } if (!isUpdate || data.email !== undefined) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (data.email && !emailRegex.test(data.email)) { errors.push('Email tidak valid'); } } if (data.gpa !== undefined && (data.gpa < 0 || data.gpa > 4)) { errors.push('IPK harus antara 0-4'); } return errors; }; // ==================== CONTROLLER METHODS ==================== // Get all students with pagination and filters exports.getAllStudents = async (req, res) => { try { const { page = 1, limit = 10, major, year, search, sortBy = 'name', order = 'asc' } = req.query; let students = await Student.getAll(); // Apply filters if (major) { students = students.filter(s => s.major.toLowerCase().includes(major.toLowerCase()) ); } if (year) { students = students.filter(s => s.year === parseInt(year)); } if (search) { const searchLower = search.toLowerCase(); students = students.filter(s => s.name.toLowerCase().includes(searchLower) || s.nim.toLowerCase().includes(searchLower) ); } // Apply sorting students.sort((a, b) => { const aValue = a[sortBy] || ''; const bValue = b[sortBy] || ''; if (order === 'desc') { return bValue.toString().localeCompare(aValue.toString()); } return aValue.toString().localeCompare(bValue.toString()); }); // Apply pagination const pageNum = parseInt(page); const limitNum = parseInt(limit); const startIndex = (pageNum - 1) * limitNum; const endIndex = pageNum * limitNum; const paginatedStudents = students.slice(startIndex, endIndex); const pagination = { page: pageNum, limit: limitNum, totalItems: students.length, totalPages: Math.ceil(students.length / limitNum), hasNextPage: endIndex < students.length, hasPrevPage: startIndex > 0 }; res.json(apiResponse(true, 'Data mahasiswa berhasil diambil', paginatedStudents, pagination)); } catch (error) { console.error('Error in getAllStudents:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } }; // Get student by ID exports.getStudentById = async (req, res) => { try { const { id } = req.params; const student = await Student.getById(id); if (!student) { return res.status(404).json(apiResponse(false, 'Mahasiswa tidak ditemukan')); } res.json(apiResponse(true, 'Data mahasiswa berhasil diambil', student)); } catch (error) { console.error('Error in getStudentById:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } }; // Create new student exports.createStudent = async (req, res) => { try { const studentData = req.body; // Validation const errors = validateStudent(studentData); if (errors.length > 0) { return res.status(400).json(apiResponse(false, 'Validasi gagal', { errors })); } // Check if NIM already exists const students = await Student.getAll(); const existingStudent = students.find(s => s.nim === studentData.nim); if (existingStudent) { return res.status(409).json(apiResponse(false, 'NIM sudah terdaftar')); } const newStudent = await Student.create(studentData); res.status(201).json(apiResponse(true, 'Mahasiswa berhasil ditambahkan', newStudent)); } catch (error) { console.error('Error in createStudent:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } }; // Update student exports.updateStudent = async (req, res) => { try { const { id } = req.params; const updateData = req.body; // Check if student exists const existingStudent = await Student.getById(id); if (!existingStudent) { return res.status(404).json(apiResponse(false, 'Mahasiswa tidak ditemukan')); } // Validation const errors = validateStudent(updateData, true); if (errors.length > 0) { return res.status(400).json(apiResponse(false, 'Validasi gagal', { errors })); } // Check if NIM already exists (if NIM is being updated) if (updateData.nim && updateData.nim !== existingStudent.nim) { const students = await Student.getAll(); const nimExists = students.find(s => s.nim === updateData.nim && s.id !== parseInt(id)); if (nimExists) { return res.status(409).json(apiResponse(false, 'NIM sudah digunakan oleh mahasiswa lain')); } } const updatedStudent = await Student.update(id, updateData); res.json(apiResponse(true, 'Data mahasiswa berhasil diperbarui', updatedStudent)); } catch (error) { console.error('Error in updateStudent:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } }; // Delete student exports.deleteStudent = async (req, res) => { try { const { id } = req.params; const studentExists = await Student.getById(id); if (!studentExists) { return res.status(404).json(apiResponse(false, 'Mahasiswa tidak ditemukan')); } const deleted = await Student.delete(id); if (!deleted) { return res.status(500).json(apiResponse(false, 'Gagal menghapus mahasiswa')); } res.json(apiResponse(true, 'Mahasiswa berhasil dihapus')); } catch (error) { console.error('Error in deleteStudent:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } }; // Get statistics exports.getStatistics = async (req, res) => { try { const students = await Student.getAll(); const stats = { totalStudents: students.length, byMajor: {}, byYear: {}, averageGPA: 0, maxGPA: 0, minGPA: 4 }; if (students.length > 0) { let totalGPA = 0; students.forEach(student => { // Count by major stats.byMajor[student.major] = (stats.byMajor[student.major] || 0) + 1; // Count by year stats.byYear[student.year] = (stats.byYear[student.year] || 0) + 1; // GPA calculations totalGPA += student.gpa; if (student.gpa > stats.maxGPA) stats.maxGPA = student.gpa; if (student.gpa < stats.minGPA) stats.minGPA = student.gpa; }); stats.averageGPA = totalGPA / students.length; } else { stats.minGPA = 0; } res.json(apiResponse(true, 'Statistik berhasil diambil', stats)); } catch (error) { console.error('Error in getStatistics:', error); res.status(500).json(apiResponse(false, 'Terjadi kesalahan server')); } };
Step 5: Routes
src/routes/students.js
const express = require('express'); const router = express.Router(); const { getAllStudents, getStudentById, createStudent, updateStudent, deleteStudent, getStatistics } = require('../controllers/studentController'); // GET /api/v1/students - Get all students (with filters & pagination) router.get('/', getAllStudents); // GET /api/v1/students/statistics - Get statistics router.get('/statistics', getStatistics); // GET /api/v1/students/:id - Get student by ID router.get('/:id', getStudentById); // POST /api/v1/students - Create new student router.post('/', createStudent); // PUT /api/v1/students/:id - Update student (full update) router.put('/:id', updateStudent); // PATCH /api/v1/students/:id - Update student (partial update) router.patch('/:id', updateStudent); // DELETE /api/v1/students/:id - Delete student router.delete('/:id', deleteStudent); module.exports = router;
Step 6: Express App Configuration
src/app.js
const express = require('express'); const cors = require('cors'); require('dotenv').config(); const studentRoutes = require('./routes/students'); const app = express(); // ==================== MIDDLEWARE ==================== // CORS app.use(cors()); // Body Parser app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Request Logger (Custom Middleware) app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${req.method} ${req.url}`); next(); }); // ==================== ROUTES ==================== const API_PREFIX = `/api/${process.env.API_VERSION || 'v1'}`; // Health Check app.get(`${API_PREFIX}/health`, (req, res) => { res.json({ status: 'healthy', service: 'Student Management API', version: '1.0.0', timestamp: new Date().toISOString() }); }); // API Documentation app.get(`${API_PREFIX}/`, (req, res) => { res.json({ message: 'Welcome to Student Management API', version: '1.0.0', endpoints: { students: `${API_PREFIX}/students`, health: `${API_PREFIX}/health` }, documentation: { getAll: 'GET /students?page=1&limit=10&major=Informatika&year=2023&search=nama', getOne: 'GET /students/:id', create: 'POST /students', update: 'PUT/PATCH /students/:id', delete: 'DELETE /students/:id', stats: 'GET /students/statistics' } }); }); // Student Routes app.use(`${API_PREFIX}/students`, studentRoutes); // ==================== ERROR HANDLING ==================== // 404 Handler app.use((req, res) => { res.status(404).json({ success: false, message: `Route not found: ${req.method} ${req.url}`, timestamp: new Date().toISOString() }); }); // Global Error Handler app.use((err, req, res, next) => { console.error('Global Error:', err); res.status(500).json({ success: false, message: 'Internal Server Error', timestamp: new Date().toISOString(), ...(process.env.NODE_ENV === 'development' && { error: err.message }) }); }); module.exports = app;
Step 7: Server Entry Point
src/server.js
const app = require('./app'); const PORT = process.env.PORT || 3000; const server = app.listen(PORT, () => { console.log(` ╔═══════════════════════════════════════════════════════════════╗ ║ 🚀 Student Management API Started ║ ╠═══════════════════════════════════════════════════════════════╣ ║ Service: Student Management API ║ ║ Version: 1.0.0 ║ ║ Environment: ${process.env.NODE_ENV || 'development'} ║ ║ Port: ${PORT} ║ ║ API URL: http://localhost:${PORT}/api/v1 ║ ║ Health Check: http://localhost:${PORT}/api/v1/health ║ ║ API Docs: http://localhost:${PORT}/api/v1 ║ ╚═══════════════════════════════════════════════════════════════╝ `); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received. Shutting down gracefully...'); server.close(() => { console.log('Process terminated'); }); }); process.on('SIGINT', () => { console.log('SIGINT received. Shutting down gracefully...'); server.close(() => { console.log('Process terminated'); }); });
3.4 Testing API dengan Postman
📋 ENDPOINTS YANG TERSEDIA:
1. GET All Students (GET /api/v1/students)
GET http://localhost:3007/api/v1/students GET http://localhost:3007/api/v1/students?page=1&limit=5 GET http://localhost:3007/api/v1/students?major=Informatika GET http://localhost:3007/api/v1/students?year=2023 GET http://localhost:3007/api/v1/students?search=Ahmad GET http://localhost:3007/api/v1/students?sortBy=gpa&order=desc
2. GET Student by ID (GET /api/v1/students/:id)
GET http://localhost:3007/api/v1/students/1
3. CREATE New Student (POST /api/v1/students)
POST http://localhost:3007/api/v1/students Content-Type: application/json { "nim": "20230003", "name": "Rina Wijaya", "email": "rina@example.com", "major": "Teknik Industri", "year": 2023, "gpa": 3.85 }
4. UPDATE Student (PUT /api/v1/students/:id)
PUT http://localhost:3007/api/v1/students/1 Content-Type: application/json { "name": "Ahmad Fauzi Updated", "gpa": 3.80 }
5. DELETE Student (DELETE /api/v1/students/:id)
DELETE http://localhost:3007/api/v1/students/1
6. GET Statistics (GET /api/v1/students/statistics)
GET http://localhost:3007/api/v1/students/statistics
7. Health Check (GET /api/v1/health)
GET http://localhost:3007/api/v1/health
🧪 Testing dengan cURL:
# Test GET all curl -X GET "http://localhost:3007/api/v1/students" # Test POST create curl -X POST http://localhost:3007/api/v1/students \ -H "Content-Type: application/json" \ -d '{"nim":"20230004","name":"Test Student","major":"Informatika","year":2024,"gpa":3.5}' # Test PUT update curl -X PUT http://localhost:3007/api/v1/students/1 \ -H "Content-Type: application/json" \ -d '{"gpa":3.9}' # Test DELETE curl -X DELETE http://localhost:3007/api/v1/students/1
3.5 Alur Data dalam Project Ini
Mari kita trace alur lengkap untuk request "Create Student":
📊 ALUR DATA: CREATE STUDENT
1. CLIENT mengirim request:
POST /api/v1/students
Body: JSON dengan data mahasiswa baru
2. ROUTES (students.js):
- Cocokkan route POST '/'
- Panggil controller: createStudent()
3. MIDDLEWARE (app.js):
- CORS check
- Parse JSON body
- Log request
- Error handling
4. CONTROLLER (studentController.js):
- Validasi input data
- Cek duplikasi NIM
- Panggil model untuk create
5. MODEL (Student.js):
- Generate ID baru
- Tambah timestamp
- Simpan ke file JSON
- Return data yang baru dibuat
6. RESPONSE ke CLIENT:
Status: 201 Created
Body: {
"success": true,
"message": "Mahasiswa berhasil ditambahkan",
"data": { ... },
"timestamp": "..."
}3.6 Latihan Praktikum
🎯 Exercise 1: Tambahkan Fitur Export Data
Buat endpoint untuk export data mahasiswa ke format CSV:
// routes/students.js - Tambah route baru router.get('/export/csv', exportToCSV); // controller/studentController.js exports.exportToCSV = async (req, res) => { try { const students = await Student.getAll(); // Convert to CSV format const csvHeader = 'ID,NIM,Name,Email,Major,Year,GPA\n'; const csvRows = students.map(s => `${s.id},${s.nim},${s.name},${s.email},${s.major},${s.year},${s.gpa}` ).join('\n'); const csvContent = csvHeader + csvRows; // Set headers untuk download res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename=students.csv'); res.send(csvContent); } catch (error) { res.status(500).json(apiResponse(false, 'Export failed')); } };
🎯 Exercise 2: Implementasi Rate Limiting
Tambahkan rate limiting untuk mencegah abuse:
// app.js const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: { success: false, message: 'Too many requests, please try again later.', timestamp: new Date().toISOString() } }); // Apply to all routes app.use(limiter);
🎯 Exercise 3: Tambahkan Database (Optional)
Replace file JSON dengan MongoDB:
// models/Student.js (MongoDB version) const mongoose = require('mongoose'); const studentSchema = new mongoose.Schema({ nim: { type: String, required: true, unique: true }, name: { type: String, required: true }, email: { type: String, required: true }, major: { type: String, required: true }, year: { type: Number, required: true }, gpa: { type: Number, min: 0, max: 4 }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); module.exports = mongoose.model('Student', studentSchema);
3.7 Best Practices yang Diimplementasikan
✅ 1. Separation of Concerns
// Routes: Hanya handle routing // Controllers: Handle business logic // Models: Handle data operations // Utils: Helper functions
✅ 2. Consistent Response Format
// Semua response format sama { "success": boolean, "message": string, "data": any, // optional "pagination": any, // optional "timestamp": string // selalu ada }
✅ 3. Proper Error Handling
// HTTP Status Codes yang tepat: // 200: OK // 201: Created // 400: Bad Request (validation error) // 404: Not Found // 409: Conflict (duplicate data) // 500: Internal Server Error
✅ 4. Input Validation
// Validasi di controller sebelum proses const errors = validateStudent(data); if (errors.length > 0) { return res.status(400).json(...); }
✅ 5. Security Considerations
// CORS enabled // Body parsing // No sensitive data in response // Error messages tidak expose internal details
3.8 Troubleshooting Common Issues
❗ Issue 1: Cannot GET /api/v1/students
Solution:
# 1. Cek server running curl http://localhost:3007/api/v1/health # 2. Cek routes configuration # Pastikan di app.js: app.use('/api/v1/students', studentRoutes) # 3. Restart server npm run dev
❗ Issue 2: Body Parser tidak bekerja
Solution:
// Pastikan middleware ini ada sebelum routes: app.use(express.json()); app.use(express.urlencoded({ extended: true }));
❗ Issue 3: CORS Error di Browser
Solution:
// Pastikan CORS middleware di enable app.use(cors()); // Atau konfigurasi lebih spesifik: app.use(cors({ origin: 'http://localhost:3000', methods: ['GET', 'POST', 'PUT', 'DELETE'] }));
❗ Issue 4: File JSON tidak terbaca
Solution:
// Pastikan path benar const dataPath = path.join(__dirname, '../data/students.json'); // Pastikan file ada dan format JSON valid // Gunakan try-catch untuk error handling try { const data = await fs.readFile(dataPath, 'utf8'); return JSON.parse(data); } catch (error) { throw new Error('Failed to read data file'); }
3.9 Project Extension Ideas
Tingkatkan project dengan menambah:
- Authentication & Authorization
- JWT tokens
- User roles (admin, user)
- Protected routes
- File Upload
- Upload foto profil mahasiswa
- Upload transkrip nilai
- Validasi file type & size
- Email Notification
- Kirim email konfirmasi
- Notifikasi update data
- Newsletter
- Real-time Features
- WebSocket untuk real-time updates
- Live notification system
- Testing Suite
- Unit tests dengan Jest
- Integration tests
- API endpoint testing
- Deployment
- Deploy ke Heroku/AWS
- CI/CD pipeline
- Environment configuration
4. Daftar Pustaka
- Express.js Documentation (2024). Express Application Structure. Diakses dari: https://expressjs.com/en/guide/routing.html
- Node.js Best Practices (2024). Project Structure Guidelines. Diakses dari: https://github.com/goldbergyoni/nodebestpractices
- REST API Tutorial (2024). Best Practices for REST API Design. Diakses dari: https://restfulapi.net/
- MDN Web Docs (2024). HTTP Status Codes. Diakses dari: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
- Postman Learning Center (2024). API Testing Best Practices. Diakses dari: https://learning.postman.com/docs/getting-started/introduction/
- Clean Code JavaScript (2024). Coding Standards. Diakses dari: https://github.com/ryanmcdermott/clean-code-javascript
- JavaScript.info (2024). Modern JavaScript Tutorial. Diakses dari: https://javascript.info/
.png)
0 Komentar