Pengantar Backend & Implementasi REST API dengan CRUD Dasar
Routing, CRUD dasar (GET/POST/PUT/DELETE), response format.
REST API
Apa itu Backend?
Backend adalah bagian server yang bertanggung jawab atas logika bisnis, validasi, pengolahan data, dan komunikasi dengan Database.
Client tidak langsung mengakses database. Semua permintaan melewati backend yang menjadi perantara antara frontend dan data.
Bayangkan aplikasi adalah sebuah restoran. Frontend adalah meja tempat pelanggan melihat menu dan memesan. Backend adalah Koki di Dapur.
Pelanggan (Client) tidak diizinkan masuk ke Gudang Makanan (Database) untuk mengambil bahan sendiri. Mereka harus meminta ke Koki. Koki inilah yang bertugas mengecek pesanan, mengambil bahan di gudang, memasaknya (logika bisnis), dan menyajikannya kembali ke pelanggan.
Kenapa Backend Wajib?
Bayangkan frontend langsung terhubung ke database tanpa backend di tengah. Tiga masalah fatal muncul:
Kredensial database dan query rahasia ikut terunduh di kode frontend publik, siapa pun bisa mengintip isi database atau menghapus tabel.
Data bisa dimanipulasi sebelum disimpan, misalnya mengubah harga barang dari Rp100.000 menjadi Rp-10.000 tanpa pengecekan ulang.
Database tidak didesain melayani ribuan akses bersamaan. Backend menjadi traffic controller yang mengamankan dan mengatur antrean.
Karena itu backend wajib berdiri di antara frontend dan data.
Apa itu REST API dan HTTP Methods?
REST adalah arsitektur yang mengatur bagaimana frontend dan backend saling berkomunikasi. Di dalam arsitektur REST ini, HTTP Methods digunakan sebagai standar protokol untuk menentukan aksi apa yang ingin dilakukan terhadap data.
Membuat atau menambahkan data baru ke server, misalnya saat mendaftarkan akun baru.
Mengambil atau melihat data dari server, misalnya menampilkan daftar produk.
Mengubah data yang sudah ada. PUT mengganti seluruh bagian, PATCH hanya sebagian.
Menghapus data yang ada di server.
- REST API Tutorial (RESTful API Design): penjelasan komprehensif konsep REST dari dasar hingga best practice.
- MDN Web Docs: Request methods: dokumentasi resmi ramah pemula, detail tiap HTTP method beserta contohnya.
Endpoint & URL
Jika HTTP Method bertindak sebagai kata kerja (apa yang ingin dilakukan: mengambil, membuat, menghapus), maka Endpoint & URL bertindak sebagai alamat / objek tujuan dari data tersebut.
URL menunjuk data mana yang dituju, dengan nama resource jamak:/api/v1/usersuntuk kumpulan pengguna, atau/api/v1/users/5untuk satu pengguna dengan ID 5.
Awalan /api/v1/ditambahkan agar saat backend diperbarui besar-besaran, aplikasi client lama tidak langsung rusak atau error.
Walaupun URL /api/v1/users/5dipakai oleh GETmaupun DELETE, hasilnya sangat berbeda. GET mengambil data user, DELETE menghapusnya secara permanen.
Cara Kirim Data dari Client
Setelah menentukan alamat URL dan metode HTTP, bagaimana cara client membungkus data yang ingin dikirim ke server? Data dikirim melalui tiga bagian utama pada Request:
Bagian yang melekat langsung pada URL untuk menunjuk identitas data spesifik, misalnya/users/5, angka 5 adalah ID unik milik user.
Bagian tambahan di akhir URL menggunakan tanda tanya (?), untuk filter, pencarian, atau pembagian halaman (pagination), misalnya/products?category=food&page=2.
Ruang khusus untuk mengirim data yang lebih kompleks atau berukuran besar dalam format JSON, paling sering dipakai saat mendaftarkan akun baru menggunakan metode POST.
Masing-masing bagian memiliki fungsi spesifik dan digunakan secara bergantian tergantung skenario aksi yang ingin dilakukan oleh client.
- GitHub: REST API Best Practices & Naming Conventions: dokumentasi ringkas tentang cara menyusun path parameter, query string, dan hierarki endpoint yang bersih sesuai standar industri.
Server Menjawab Dengan Format JSON
Setelah backend selesai memproses request, saatnya backend memberikan Response. Agar bisa dipahami platform apa pun, data dibungkus dengan format universal yaitu JSON (JavaScript Object Notation).
Membaca Jawaban Server: HTTP Status Code
Bersamaan dengan data JSON, server selalu menyelipkan HTTP Status Code. Fungsinya untuk memberi tahu client secara instan apakah permintaan berhasil, ditolak, atau gagal total, tanpa harus membaca seluruh isi JSON terlebih dahulu.
Berikut contoh yang paling sering muncul:
Permintaan diproses tanpa masalah. Contoh:200 OKsukses umum (misal berhasil mengambil data produk),201 Createdsaat akun berhasil didaftarkan di database.
"Aku nggak bisa proses ini karena client yang salah kirim." Contoh:400 input salah format,401 belum login akses data rahasia,404 endpoint/data tidak ada.
"Permintaanmu benar, tapi sistem kami (backend) yang sedang rusak." Contoh:500 Internal Server Errorada bug/crash di backend atau database sedang mati.
- MDN Web Docs: HTTP response status codes: referensi resmi arti mendetail setiap status code.
- HTTP Cats atau HTTP Dogs: visualisasi tiap status code dengan meme kucing/anjing.
Hands-on 1
Setup Project & Instalasi Express
Sebelum menulis kode backend, kita perlu membuat folder khusus dan menyiapkan proyek Node.js terlebih dahulu.
Kode Hands-on di GitHubSetelah setup selesai, struktur folder proyek kita:
my-api/
├─ package.json
├─ tsconfig.json
└─ src/
└─ index.tsFile package.jsonbertindak sebagai "identitas" proyek yang mencatat seluruh pustaka (dependencies) yang kita pakai. Ia dibuat otomatis oleh perintahnpm init -y, dan npm install expressmenyimpan Express ke dalamnya.
- Node.js Official Download: installer resmi Node.js & NPM sesuai sistem operasi (Windows/Mac/Linux).
- Express: Installing: panduan resmi tahapan setup awal proyek Express.
- npmjs.com: About npm: dokumentasi singkat fungsi NPM dalam ekosistem Node.js.
Menjalankan TypeScript dengan tsx
Dengan tsx, kita bisa menjalankan file.tslangsung tanpa langkah kompilasi terpisah.Dependencies terisi otomatis oleh npm install; yang perlu ditambahkan manual adalah bagian scripts(dan "type"/"main"). Agar mudah, kita daftarkan dua script,startdandev, di dalampackage.json:
{
"name": "my-api",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"express": "^5.2.1"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^26.5.1",
"tsx": "^4.23.13",
"typescript": "^7.0.2"
}
}Buat juga tsconfig.jsonagar TypeScript mengenali lingkungan Node (seperti processdan modul bawaan) serta import bergaya ./file.ts, sehingga editor/LSP tidak menandai error:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}"main": "src/index.ts"mengarahkan main entry point proyek ke filesrc/index.ts."start"menjalankan server sekali:tsx src/index.ts."dev"memakaitsx watchagar server otomatis memuat ulang (hot-reload) setiap file disimpan.
Membuat Endpoint Pertama (GET /health)
Buat file baru bernama src/index.ts di dalam folder src proyekmu. File ini adalah entry point di mana kita menyusun aturan server. Di tahap ini kita membuat Health Check Endpoint, yaitu endpoint sederhana untuk memastikan server aktif dan merespons.
import express, { type Request, type Response } from 'express';
const app = express();
const PORT: number = 3000;
// Health Check Endpoint
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({
status: "success",
message: "Server is running",
timestamp: new Date()
});
});
// Menyalakan server
app.listen(PORT, () => {
console.log(`Server berjalan di http://localhost:${PORT}`);
});Menyalakan Server & Pengujian di Browser
Setelah kode di file src/index.ts selesai ditulis, saatnya menghidupkan mesin server lokal dan memverifikasi apakah endpoint kita berjalan sesuai harapan.
- Jalankan Server di Terminal
Jika berhasil, terminal akan menampilkan pesan:
Server berjalan di http://localhost:3000 - Uji Endpoint di Browser
http://localhost:3000/health - Hasil yang Diharapkan (JSON Response).response.jsonjson
{ "status": "success", "message": "Server is running", "timestamp": "2026-09-09T16:46:09.000Z" }
Arsitektur Layered
Mengapa Butuh Layer?
Kita telah menulis endpoint/health langsung di satu file. Cara tersebut sangat oke untuk skenario sederhana. Namun, bagaimana jika aplikasi mulai menangani fitur kompleks seperti autentikasi, transaksi, validasi input, query database, dan banyak lagi?
Jika semua logika tersebut ditumpuk di dalam satu file atau satu fungsi handler, kode akan menjadi "Spaghetti Code" yang gemuk dan sulit diperbaiki.
Lalu, bagaimana cara menjaga kode tetap bersih dan mudah dipelajari saat fitur mulai bertambah banyak?
Solusi: Layered Architecture
Kita membagi kode ke dalam beberapa lapisan. Setiap layer hanya fokus pada satu tugas spesifik (Separation of Concerns):
- Router: menangkap request HTTP dan meneruskannya ke fungsi controller yang tepat berdasarkan kombinasi HTTP Method dan URL.
- Controller: menerima input dari client, memeriksa kelengkapan data, dan menyusun response (status code & JSON).
- DTO (Data Transfer Object): objek pembawa data yang menyusun/menyaring field sebelum dikirim antar layer maupun ke client, supaya struktur data konsisten dan tidak "bocor" (hanya field yang perlu yang dikirim).
- Service: tempat khusus untuk logika bisnis dan aturan aplikasi.
- Repository / Data Access: khusus menangani perintah SQL/query untuk menyimpan atau mengambil data dari Database.
Hands-on 2
Repository
Karena ini contoh dasar, kita gunakan mock data yaitu interface & data didefinisikan langsung di file ini.
Kode Hands-on di GitHubHands-on 2 melanjutkan project yang sama (my-api). Kita menambahkan folder untuk arsitektur berlapis:
my-api/
├─ package.json
├─ tsconfig.json
└─ src/
├─ index.ts # diubah: daftarkan router
├─ routes/
│ └─ stallRouter.ts # baru
├─ controllers/
│ └─ stallController.ts # baru
├─ services/
│ └─ stallService.ts # baru
├─ repositories/
│ └─ stallRepository.ts # baru
└─ dtos/
└─ stallDto.ts # baru// Interface simulasi schema tabel Stalls
export interface Stall {
id: number;
name: string;
location: string;
rating: number;
}
// Mock Data simulasi isi tabel Stalls
const stallsData: Stall[] = [
{ id: 1, name: "Kwetiau Goreng", location: "Kantin Alif FKIP", rating: 4.8 },
{ id: 2, name: "Bakso Pasca", location: "Kantin Bakso Pasca", rating: 4.5 },
{ id: 3, name: "Paket Chikcen Steak Hot Plate", location: "Kantin FK", rating: 4.8 }
];
export class StallRepository {
async findAll(): Promise<Stall[]> {
return stallsData;
}
async findById(id: number): Promise<Stall | undefined> {
return stallsData.find(stall => stall.id === id);
}
}- MDN: Asynchronous JavaScript: panduan konsep non-blocking & async/await.
- MDN: Using Promises: penulisan tipe
Promise<T>.
DTO (Data Transfer Object)
DTO berfungsi sebagai "kontrak data" untuk menentukan struktur pasti data yang akan diterima atau dikembalikan oleh server.
// Interface kontrak data response untuk client
export interface StallResponseDto {
id: number;
name: string;
location: string;
rating: number;
isPopular: boolean; // Property tambahan dari kalkulasi logika bisnis
}- Memisahkan struktur internal database/repository dengan struktur data yang disajikan ke publik/frontend.
- Penambahan field khusus tampilan (seperti
isPopular) bisa dilakukan tanpa mengacak-acak struktur tabel asli di repository.
Service
Setelah kontrak DTO siap, kita buat Service Layer yang memanggil Repository untuk mengambil data mentah, lalu menerapkan logika bisnisisPopular), dan mengembalikan data dalam format StallResponseDto.
import { StallRepository } from '../repositories/stallRepository.ts';
import { type StallResponseDto } from '../dtos/stallDto.ts';
export class StallService {
constructor(private stallRepository: StallRepository = new StallRepository()) {}
async getAllStalls(): Promise<StallResponseDto[]> {
const stalls = await this.stallRepository.findAll();
// Logika Bisnis: Menambahkan penanda 'isPopular' jika rating >= 4.7
return stalls.map(stall => ({
...stall,
isPopular: stall.rating >= 4.7
}));
}
async getStallById(id: number): Promise<StallResponseDto> {
const stall = await this.stallRepository.findById(id);
// Logika Bisnis: Jika data warung tidak ditemukan, lempar error khusus
if (!stall) {
throw new Error("STALL_NOT_FOUND");
}
return {
...stall,
isPopular: stall.rating >= 4.7
};
}
}Controller
Controller membungkus logika bisnis di balik HTTP: menerimareq, meneruskan parameter ke Service, lalu menyusunresberformat JSON beserta status code yang sesuai.
import { type Request, type Response } from 'express';
import { StallService } from '../services/stallService.ts';
export class StallController {
constructor(private stallService: StallService = new StallService()) {}
// Menggunakan arrow function agar binding konteks 'this' tetap terjaga di Router
getStalls = async (req: Request, res: Response): Promise<Response> => {
try {
const stalls = await this.stallService.getAllStalls();
return res.status(200).json({
status: "success",
data: stalls
});
} catch (error) {
return res.status(500).json({
status: "error",
message: "Gagal mengambil data warung"
});
}
};
getStallById = async (req: Request, res: Response): Promise<Response> => {
try {
const id = Number(req.params.id);
const stall = await this.stallService.getStallById(id);
return res.status(200).json({
status: "success",
data: stall
});
} catch (error: any) {
if (error.message === "STALL_NOT_FOUND") {
return res.status(404).json({
status: "fail",
message: "Data warung tidak ditemukan"
});
}
return res.status(500).json({
status: "error",
message: "Terjadi kesalahan pada server"
});
}
};
}Router
Layer ini bertugas memetakan pasangan HTTP Method (mis. GET) dan URL Path (mis. /atau /:id) ke fungsi handler yang sesuai di StallController.
import { Router } from 'express';
import { StallController } from '../controllers/stallController.ts';
const stallRouter = Router();
const stallController = new StallController();
// Memetakan endpoint ke metode controller
stallRouter.get('/', stallController.getStalls);
stallRouter.get('/:id', stallController.getStallById);
export { stallRouter };Integrasi ke Entry Point
Seluruh lapisan (Repository → Service → Controller → Router) kini digabungkan ke file entry point utama server, src/index.ts.
import express, { type Request, type Response, type Application } from 'express';
import { stallRouter } from './routes/stallRouter.ts';
const app: Application = express();
const PORT: number = 3000;
app.use(express.json());
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: "success", message: "Server running" });
});
// Pendaftaran Module Router dengan URL Prefix
app.use('/api/v1/stalls', stallRouter);
app.listen(PORT, () => {
console.log(`Server berjalan di http://localhost:${PORT}`);
});Pengujian Endpoint
Jalankan server dengannpm run dev, lalu buka browser dan uji endpoint berikut:
Ambil Semua Warung.
GET http://localhost:3000/api/v1/stallsHasil: mengembalikan daftar warung lengkap dengan properti
isPopular.Ambil Detail Warung (ID Ada).
GET http://localhost:3000/api/v1/stalls/1Hasil: mengembalikan objek warung Kwetiau Goreng (Status 200).
Ambil Detail Warung (ID Tidak Ada).
GET http://localhost:3000/api/v1/stalls/99Hasil: mengembalikan
{"status":"fail","message":"Data warung tidak ditemukan"}(Status 404).
