Pengembangan Full-Stack dengan Angular, Node.js, dan MySQL: 100 Contoh dari Nol hingga Profesional
Aplikasi full-stack terdiri atas tiga bagian yang harus saling sesuai: front end Angular yang dipakai pengguna di browser, API Node.js yang menerapkan aturan, dan basis data MySQL yang menjaga data tetap aman. Artikel ini membangun satu toko kecil dari folder kosong hingga rilis yang siap produksi dalam 100 contoh. Anda akan menyiapkan proyek, mempelajari dasar Node.js dan Express untuk membangun REST API, merancang tabel MySQL dan menulis kueri yang aman, menghubungkan API ke basis data dengan transaksi, memvalidasi input, menambahkan login dengan kata sandi ter-hash dan token JWT, membangun komponen, layanan, rute, formulir, dan interseptor Angular, menghubungkan semuanya lewat proxy dan berkas environment, menguji setiap lapisan, mengemas tumpukan dengan Docker, dan menyiapkannya untuk produksi. Catatan jujur: MySQL dan Angular CLI tidak terpasang di komputer yang dipakai menyiapkan artikel ini, jadi 100 contoh tersebut tidak dijalankan di sini. Kode mengikuti sintaks Node.js, Express, mysql2, Angular, dan MySQL saat ini, tetapi jalankan sendiri setiap bagiannya sebelum Anda mengandalkannya.
Yang Anda butuhkan
Node.js 20 atau yang lebih baru beserta npm, MySQL 8.4 (atau Docker untuk menjalankannya dalam kontainer), Angular CLI, dan editor kode seperti Visual Studio Code. Pengetahuan dasar JavaScript dan HTML akan membantu. Gunakan kata sandi contoh seperti <strong-password> dan domain contoh seperti shop.example.com, jangan pernah commit berkas .env yang asli, dan jangan menempelkan kredensial asli ke dalam kode atau perintah.
Cara memakai contoh
Contoh bash memakai npm, Angular CLI, dan Docker. Contoh JavaScript dan TypeScript masuk ke berkas proyek Node.js dan Angular yang disebut dalam teks. Contoh SQL dijalankan di MySQL 8.4, dan contoh HTML masuk ke templat Angular.
Topik 1: Penyiapan Proyek
Mulailah dengan struktur yang rapi: satu folder untuk API, satu untuk aplikasi Angular, dan satu untuk skrip basis data. Struktur yang baik memudahkan setiap bagian untuk diuji, dideploy, dan dijelaskan kepada orang baru.
Tiga bagian dalam satu repositori
Simpan API, aplikasi web, dan skrip basis data dalam satu repositori agar satu perubahan dapat memperbarui ketiganya sekaligus. Setiap bagian memiliki berkas paket dan perintahnya sendiri.
Alat dan versi
Periksa Node.js, npm, dan MySQL terlebih dahulu. Node.js 20 atau yang lebih baru disarankan karena menyertakan pelari uji bawaan dan fetch modern. Angular CLI menghasilkan kode dan menjalankan server pengembangan.
Rahasia tidak masuk Git
Kata sandi dan kunci penandatanganan disimpan dalam berkas .env yang tercantum di .gitignore. Commit berkas contoh dengan nilai pengganti agar orang lain tahu pengaturan apa yang diperlukan.
Ilustrasi
Contoh 1: Periksa alat
Pastikan Node.js, npm, dan MySQL sudah terpasang.
node --version
npm --version
mysql --version
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 2: Buat folder proyek
Pisahkan API, aplikasi web, dan basis data.
mkdir shop-fullstack && cd shop-fullstack
mkdir api web db
git init
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 3: Siapkan API Node.js
Pasang Express, driver MySQL, dan dotenv.
cd api
npm init -y
npm install express mysql2 dotenv
npm install --save-dev nodemon
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 4: Pasang Angular CLI
Buat front end Angular dengan routing.
npm install -g @angular/cli
cd ..
ng new web --routing --style=css --skip-git
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 5: Tata letak proyek
Simpan tiga bagian di folder yang jelas.
shop-fullstack/
api/ Node.js + Express + MySQL
web/ Angular front end
db/ SQL schema and seed data
docker-compose.yml
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 6: Jauhkan rahasia dari Git
Abaikan .env dan commit berkas contoh sebagai gantinya.
cat > .gitignore <<'EOF'
node_modules/
.env
dist/
EOF
cat > api/.env.example <<'EOF'
DB_HOST=localhost
DB_USER=shop
DB_PASSWORD=<strong-password>
DB_NAME=shop
PORT=3000
EOF
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 2: Dasar-Dasar Node.js
Node.js menjalankan JavaScript di luar browser. Pelajari modul, kode asinkron, variabel lingkungan, dan sekumpulan kecil gagasan yang digunakan setiap berkas server.
Modul dan kode asinkron
Setiap berkas adalah modul yang mengekspor apa yang dibutuhkan berkas lain. Sebagian besar pekerjaan server bersifat asinkron, jadi gunakan Promise dengan async dan await, dan tangani galat dengan try dan catch agar satu kegagalan tidak menjatuhkan seluruh proses.
Konfigurasi dari lingkungan
Baca pengaturan seperti port dan alamat basis data dari process.env. Paket dotenv memuat berkas .env selama pengembangan, sedangkan server produksi biasanya menerima variabel dari platform.
Skrip dan event loop
Skrip npm memberi setiap anggota tim perintah yang sama. Ingat bahwa satu operasi sinkron yang lambat akan memblokir setiap permintaan, jadi jauhkan pekerjaan berat dari penangan permintaan.
Contoh 7: Baca berkas dengan Node.js
Pembacaan berkas sinkron dan kompromisnya.
const fs = require("node:fs");
const text = fs.readFileSync("notes.txt", "utf8");
console.log(text.split("\n").length, "lines");
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 8: Baca berkas secara asinkron
Gunakan Promise dengan async dan await.
const fs = require("node:fs/promises");
async function main() {
const text = await fs.readFile("notes.txt", "utf8");
console.log(text.trim());
}
main().catch(console.error);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 9: Modul di Node.js
Ekspor fungsi dan impor di berkas lain.
// math.js
exports.add = (a, b) => a + b;
// main.js
const { add } = require("./math");
console.log(add(2, 3));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 10: Variabel lingkungan
Baca pengaturan dari .env dengan dotenv.
require("dotenv").config();
const port = Number(process.env.PORT || 3000);
console.log("port", port);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 11: Array dan filter
Saring dan ubah data seperti yang dilakukan API.
const orders = [
{ id: 1, total: 120 },
{ id: 2, total: 80 },
{ id: 3, total: 200 },
];
const big = orders.filter((o) => o.total >= 100).map((o) => o.id);
console.log(big); // [1, 3]
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 12: Promise dan await
Tunggu pekerjaan tanpa memblokir server.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function run() {
console.log("start");
await wait(500);
console.log("done after 500 ms");
}
run();
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 13: Galat dengan try dan catch
Tangani kegagalan dan kembalikan nilai aman.
async function readConfig(path) {
try {
const fs = require("node:fs/promises");
return JSON.parse(await fs.readFile(path, "utf8"));
} catch (err) {
console.error("could not read config:", err.message);
return {};
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 14: Skrip npm
Jalankan, kembangkan, dan uji API dengan satu perintah.
{
"name": "api",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "node --test"
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 3: REST API dengan Express
Express memetakan metode dan jalur HTTP ke fungsi. REST API memakai GET untuk membaca, POST untuk membuat, PUT atau PATCH untuk mengubah, dan DELETE untuk menghapus, lalu mengembalikan JSON yang dapat ditampilkan front end.
Rute, parameter, dan body
app.get, app.post, dan sejenisnya mencocokkan metode dan jalur. Baca parameter URL dari req.params, query string dari req.query, dan body JSON dari req.body setelah app.use(express.json()).
Router dan middleware
Router mengelompokkan rute dari satu sumber daya dalam satu berkas. Middleware berjalan berurutan untuk setiap permintaan, sehingga cocok untuk tempat pencatatan log, autentikasi, dan penanganan galat.
Kode status yang bermakna
Kembalikan 200 untuk pembacaan, 201 untuk sumber daya yang dibuat, 204 untuk penghapusan tanpa body, 400 untuk input yang buruk, 401 saat pengguna belum login, 403 saat tidak diizinkan, dan 404 saat item tidak ada.
Contoh 15: Server Express pertama
Balas permintaan dalam beberapa baris.
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Shop API is running");
});
app.listen(3000, () => console.log("listening on http://localhost:3000"));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 16: Rute yang mengembalikan JSON
Kirim data terstruktur untuk front end.
app.get("/api/health", (req, res) => {
res.json({ status: "ok", time: new Date().toISOString() });
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 17: Parameter rute dan query string
Baca nilai dari URL.
app.get("/api/products/:id", (req, res) => {
const id = Number(req.params.id);
const detail = req.query.detail === "1";
res.json({ id, detail });
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 18: Parse body JSON
Terima data dari formulir atau Angular.
app.use(express.json());
app.post("/api/products", (req, res) => {
const { name, price } = req.body;
res.status(201).json({ name, price });
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 19: Atur rute dengan router
Simpan setiap fitur di berkasnya sendiri.
// routes/products.js
const { Router } = require("express");
const router = Router();
router.get("/", (req, res) => res.json([]));
router.get("/:id", (req, res) => res.json({ id: req.params.id }));
module.exports = router;
// server.js
app.use("/api/products", require("./routes/products"));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 20: Middleware pencatat permintaan
Catat metode, jalur, status, dan waktu.
app.use((req, res, next) => {
const started = Date.now();
res.on("finish", () => {
console.log(req.method, req.originalUrl, res.statusCode, Date.now() - started, "ms");
});
next();
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 21: Penangan galat terpusat
Kembalikan satu format galat dari semua rute.
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({ error: "Something went wrong" });
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 22: 404 untuk rute yang tidak dikenal
Jawab dengan jelas jika URL tidak ada.
app.use((req, res) => {
res.status(404).json({ error: "Not found" });
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 4: Dasar MySQL dan SQL
MySQL menyimpan data dalam tabel dengan kolom yang bertipe, kunci, dan relasi. SQL membuat, membaca, mengubah, dan menghapus baris, dan desain yang baik mencegah banyak bug sebelum kode apa pun dijalankan.
Basis data, pengguna, dan hak akses minimum
Buat basis data untuk toko dan pengguna yang hanya dapat membaca dan menulis basis data itu. Jangan biarkan aplikasi terhubung sebagai akun root, karena kebocoran kata sandi akan mengekspos seluruh server.
Tabel, kunci, dan tipe
Beri setiap tabel kunci utama, pilih tipe yang sesuai dengan datanya, misalnya DECIMAL untuk uang, dan gunakan kunci asing agar pesanan tidak dapat merujuk ke produk yang tidak ada. Gunakan UNIQUE untuk nilai yang tidak boleh berulang, seperti alamat email.
Membaca dan mengubah baris
SELECT dengan WHERE, ORDER BY, dan LIMIT mengembalikan baris yang Anda butuhkan. UPDATE dan DELETE harus selalu memiliki klausa WHERE yang diuji lebih dulu dengan SELECT, karena kondisi yang hilang akan mengubah setiap baris.
Contoh 23: Buat basis data dan pengguna
Berikan aplikasi hanya hak yang dibutuhkan.
CREATE DATABASE IF NOT EXISTS shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'shop'@'localhost' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop'@'localhost';
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 24: Buat tabel
Tentukan kolom, kunci, dan nilai bawaan.
USE shop;
CREATE TABLE products (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 25: Sisipkan baris
Tambahkan beberapa baris dalam satu pernyataan.
INSERT INTO products (name, price, stock)
VALUES ('Keyboard', 790.00, 12),
('Mouse', 350.00, 30),
('Monitor', 5490.00, 4);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 26: SELECT dengan filter dan urutan
Baca hanya baris yang dibutuhkan, dengan urutan.
SELECT id, name, price
FROM products
WHERE stock > 0
ORDER BY price DESC;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 27: Perbarui satu baris
Ubah nilai dengan klausa WHERE.
UPDATE products
SET price = 690.00
WHERE name = 'Keyboard';
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 28: Hapus baris dengan aman
Batasi penghapusan hanya pada baris yang dimaksud.
DELETE FROM products
WHERE id = 3
LIMIT 1;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 29: JOIN dan agregasi
Hitung penjualan per produk dengan GROUP BY.
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
product_id INT UNSIGNED NOT NULL,
quantity INT NOT NULL,
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);
SELECT p.name, SUM(o.quantity) AS sold
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY p.name
ORDER BY sold DESC;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 30: Indeks kolom pencarian
Percepat pencarian dan periksa rencananya dengan EXPLAIN.
CREATE INDEX idx_products_name ON products (name);
EXPLAIN SELECT * FROM products WHERE name = 'Mouse';
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 5: Akses Data dari Node.js
API berkomunikasi dengan MySQL melalui connection pool. Kueri berparameter memisahkan input pengguna dari SQL, transaksi menjaga perubahan yang terkait tetap bersama, dan paginasi membuat respons tetap kecil.
Connection pool
Pool menyimpan sekumpulan koneksi terbuka dan memberikan satu koneksi kepada setiap permintaan, lalu mengembalikannya. Buat pool sekali saat startup dan gunakan ulang, bukan membuka koneksi baru untuk setiap permintaan.
Placeholder mencegah SQL injection
Jangan pernah menggabungkan input pengguna ke dalam teks SQL. Tulis ? di kueri dan berikan nilainya dalam array, lalu driver akan meng-escape-nya. Kebiasaan ini saja mencegah salah satu celah keamanan paling umum di web.
Transaksi dan paginasi
Transaksi mengelompokkan beberapa pernyataan sehingga semuanya berhasil atau tidak satu pun berhasil, yang penting saat pesanan mengurangi stok dan membuat baris pesanan. Gunakan LIMIT dan OFFSET, atau lebih baik keyset, untuk mengembalikan satu halaman setiap kali.
Contoh 31: Pool koneksi
Pakai ulang koneksi, jangan buka satu per permintaan.
const mysql = require("mysql2/promise");
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
});
module.exports = pool;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 32: Kueri dari Node.js
Kirim SQL dari rute dan kembalikan barisnya.
const pool = require("./db");
app.get("/api/products", async (req, res, next) => {
try {
const [rows] = await pool.query("SELECT id, name, price, stock FROM products ORDER BY name");
res.json(rows);
} catch (err) {
next(err);
}
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 33: Placeholder melawan injeksi SQL
Jangan pernah menyambung input pengguna ke teks SQL.
// Unsafe: never build SQL by joining user input
// const [rows] = await pool.query("SELECT * FROM products WHERE name = '" + name + "'");
// Safe: the driver escapes the value
const [rows] = await pool.query("SELECT * FROM products WHERE name = ?", [name]);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 34: Sisipkan dan kembalikan id baru
Buat baris dan beri tahu klien id-nya.
app.post("/api/products", async (req, res, next) => {
try {
const { name, price, stock = 0 } = req.body;
const [result] = await pool.execute(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
[name, price, stock]
);
res.status(201).json({ id: result.insertId, name, price, stock });
} catch (err) {
next(err);
}
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 35: Perbarui dengan pemeriksaan
Kembalikan 404 jika tidak ada baris yang cocok.
app.put("/api/products/:id", async (req, res, next) => {
try {
const { price, stock } = req.body;
const [result] = await pool.execute(
"UPDATE products SET price = ?, stock = ? WHERE id = ?",
[price, stock, req.params.id]
);
if (result.affectedRows === 0) return res.status(404).json({ error: "Not found" });
res.json({ updated: true });
} catch (err) {
next(err);
}
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 36: Hapus dengan pemeriksaan
Kembalikan 204 atau 404 sesuai hasilnya.
app.delete("/api/products/:id", async (req, res, next) => {
try {
const [result] = await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
res.status(result.affectedRows ? 204 : 404).end();
} catch (err) {
next(err);
}
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 37: Transaksi
Pesan stok dan buat pesanan bersama, atau tidak sama sekali.
async function placeOrder(productId, quantity) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [stock] = await conn.execute("SELECT stock FROM products WHERE id = ? FOR UPDATE", [productId]);
if (stock[0].stock < quantity) throw new Error("not enough stock");
await conn.execute("UPDATE products SET stock = stock - ? WHERE id = ?", [quantity, productId]);
await conn.execute("INSERT INTO orders (product_id, quantity) VALUES (?, ?)", [productId, quantity]);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 38: Paginasi
Kembalikan hasil per halaman, bukan seluruh tabel.
app.get("/api/products", async (req, res, next) => {
const page = Math.max(1, Number(req.query.page) || 1);
const size = Math.min(50, Number(req.query.size) || 20);
try {
const [rows] = await pool.execute(
"SELECT id, name, price FROM products ORDER BY id LIMIT ? OFFSET ?",
[size, (page - 1) * size]
);
res.json({ page, size, items: rows });
} catch (err) {
next(err);
}
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 6: Validasi dan Penanganan Galat
API tidak boleh mempercayai browser. Periksa setiap input, kembalikan pesan yang jelas untuk kesalahan, dan ubah galat basis data menjadi respons yang dapat ditampilkan front end tanpa membocorkan detail internal.
Validasi di server
Pemeriksaan di Angular meningkatkan pengalaman pengguna, tetapi server harus memeriksa lagi karena siapa pun dapat mengirim permintaan langsung. Pustaka skema menjelaskan kolom sekali dan melaporkan semua masalah dalam satu respons.
Satu format galat
Gunakan penangan galat terpusat yang mengembalikan bentuk JSON yang sama untuk setiap kegagalan. Petakan galat basis data yang dikenal, seperti kunci ganda, ke 409, dan catat galat lengkapnya di server, tetapi kirim ke klien hanya pesan yang aman.
Galat dari rute asinkron
Express 4 tidak menangkap Promise yang ditolak dari penangan rute secara otomatis. Bungkus rute asinkron agar galatnya sampai ke penangan galat, atau gunakan Express 5 yang menanganinya untuk Anda.
Contoh 39: Validasi input
Periksa kolom sebelum sampai ke basis data.
function validateProduct(body) {
const errors = [];
if (typeof body.name !== "string" || body.name.trim().length < 2) errors.push("name must be at least 2 characters");
if (typeof body.price !== "number" || body.price < 0) errors.push("price must be a number >= 0");
return errors;
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 40: Kembalikan 400 untuk input buruk
Jelaskan setiap galat validasi kepada klien.
app.post("/api/products", async (req, res, next) => {
const errors = validateProduct(req.body);
if (errors.length) return res.status(400).json({ errors });
// ...insert as in example 34
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 41: Validasi dengan skema
Deskripsikan data sekali dan pakai ulang aturannya.
const { z } = require("zod");
const productSchema = z.object({
name: z.string().min(2).max(120),
price: z.number().nonnegative(),
stock: z.number().int().nonnegative().default(0),
});
const parsed = productSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues });
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 42: Ubah galat basis data menjadi respons
Petakan duplikat dan referensi hilang ke kode yang jelas.
app.use((err, req, res, next) => {
if (err.code === "ER_DUP_ENTRY") return res.status(409).json({ error: "Already exists" });
if (err.code === "ER_NO_REFERENCED_ROW_2") return res.status(422).json({ error: "Related item not found" });
next(err);
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 43: Kelas galat sendiri
Bawa status HTTP bersama pesan galat.
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
// usage
throw new HttpError(404, "Product not found");
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 44: Pembungkus rute async
Teruskan galat dari rute async ke penangan.
const asyncRoute = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/api/products/:id", asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT * FROM products WHERE id = ?", [req.params.id]);
if (!rows.length) throw new HttpError(404, "Product not found");
res.json(rows[0]);
}));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 45: CORS untuk server pengembangan Angular
Izinkan browser memanggil API dari port lain.
const cors = require("cors");
app.use(cors({ origin: "http://localhost:4200" }));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 46: Header keamanan
Atur header HTTP yang aman dengan helmet.
const helmet = require("helmet");
app.use(helmet());
app.disable("x-powered-by");
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 7: Autentikasi dan Otorisasi
Autentikasi membuktikan siapa pengguna itu. Otorisasi menentukan apa yang boleh dilakukan pengguna tersebut. Simpan kata sandi sebagai hash, terbitkan token yang berumur pendek, dan periksa peran di setiap rute yang dilindungi.
Kata sandi ter-hash
bcrypt mengubah kata sandi menjadi hash satu arah dengan salt bawaan dan faktor biaya. Simpan hash saja, bandingkan dengan bcrypt.compare saat login, dan jangan pernah mencatat kata sandi atau mengembalikannya dalam respons.
Token dan masa berlakunya
JSON Web Token membawa ID dan peran pengguna, ditandatangani dengan rahasia. Buat masa berlakunya singkat, misalnya satu jam, dan gunakan refresh token yang dapat dicabut untuk sesi yang lebih panjang. Simpan refresh token sebagai hash di basis data.
Peran dan perlindungan rute
Verifikasi token di middleware, lalu periksa peran untuk tindakan khusus admin. Ingat bahwa front end dapat menyembunyikan tombol, tetapi hanya server yang dapat menegakkan aturan.
Contoh 47: Hash kata sandi
Simpan hash bcrypt, jangan kata sandi aslinya.
const bcrypt = require("bcrypt");
async function hashPassword(plain) {
return bcrypt.hash(plain, 12);
}
async function checkPassword(plain, hash) {
return bcrypt.compare(plain, hash);
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 48: Tabel pengguna
Simpan email unik, hash kata sandi, dan peran.
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role ENUM('customer', 'admin') NOT NULL DEFAULT 'customer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 49: Daftarkan pengguna
Buat akun dengan kata sandi yang sudah di-hash.
app.post("/api/auth/register", asyncRoute(async (req, res) => {
const { email, password } = req.body;
const hash = await hashPassword(password);
const [result] = await pool.execute(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
[email.toLowerCase(), hash]
);
res.status(201).json({ id: result.insertId, email });
})));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 50: Masuk dan terbitkan token
Kembalikan JWT tertandatangani setelah kata sandi benar.
const jwt = require("jsonwebtoken");
app.post("/api/auth/login", asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT id, password_hash, role FROM users WHERE email = ?", [req.body.email]);
if (!rows.length || !(await checkPassword(req.body.password, rows[0].password_hash))) {
throw new HttpError(401, "Wrong email or password");
}
const token = jwt.sign({ sub: rows[0].id, role: rows[0].role }, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({ token });
})));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 51: Verifikasi JWT di middleware
Baca token Bearer dan tolak token yang tidak valid atau kedaluwarsa.
const jwt = require("jsonwebtoken");
function requireAuth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "Invalid or expired token" });
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 52: Izinkan hanya peran tertentu
Kembalikan 403 jika pengguna tidak memiliki peran tersebut.
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 53: Lindungi rute
Wajibkan login untuk pesanan dan batasi penghapusan untuk admin.
app.get("/api/orders", requireAuth, asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT id, product_id, quantity FROM orders WHERE user_id = ?", [req.user.sub]);
res.json(rows);
})));
app.delete("/api/products/:id", requireAuth, requireRole("admin"), asyncRoute(async (req, res) => {
await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
res.status(204).end();
}));
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 54: Simpan refresh token
Simpan hash setiap refresh token agar bisa dicabut.
CREATE TABLE refresh_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
revoked TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 8: Komponen dan Templat Angular
Angular membangun antarmuka dari komponen. Setiap komponen memiliki kelas yang berisi logika, templat yang berisi markup, dan gaya. Komponen mandiri, alur kontrol, dan binding adalah cara modern untuk menulisnya.
Komponen mandiri
Komponen mandiri mendaftarkan impornya sendiri dan tidak memerlukan NgModule. Buat dengan ng generate component, lalu gunakan selektornya di templat lain untuk menampilkannya.
Binding
Interpolasi menampilkan nilai di antara kurung kurawal ganda. Property binding mengatur properti DOM di antara kurung siku, dan event binding memanggil metode di antara kurung biasa. Binding dua arah juga bisa dipakai, tetapi aliran data satu arah lebih mudah di-debug.
Alur kontrol
Gunakan @if untuk menampilkan konten secara kondisional, @for dengan ekspresi track untuk mengulang item, dan @switch untuk memilih salah satu dari beberapa tampilan. Pelacakan berdasarkan id menjaga halaman tetap efisien saat daftar berubah.
Contoh 55: Komponen Angular mandiri
Komponen yang tidak memerlukan NgModule.
import { Component } from "@angular/core";
@Component({
selector: "app-hello",
standalone: true,
template: `<h1>Welcome to the shop</h1>`,
})
export class HelloComponent {}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 56: Data binding di templat
Tampilkan nilai, ikat atribut, dan tangani klik.
<h2>{{ product.name }}</h2>
<p>Price: {{ product.price | currency:'THB' }}</p>
<button [disabled]="product.stock === 0" (click)="addToCart(product)">Add to cart</button>
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 57: Alur kontrol dengan @if dan @for
Tampilkan daftar dan kondisi kosong di templat.
@if (products.length > 0) {
<ul>
@for (p of products; track p.id) {
<li>{{ p.name }} ({{ p.stock }} left)</li>
}
</ul>
} @else {
<p>No products yet.</p>
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 58: Input dan output
Kirim data ke komponen anak dan kembalikan event ke induk.
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({ selector: "app-product-card", standalone: true, template: "" })
export class ProductCardComponent {
@Input({ required: true }) product!: { id: number; name: string; price: number };
@Output() added = new EventEmitter<number>();
add() {
this.added.emit(this.product.id);
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 59: Menjalankan aplikasi mandiri
Sediakan router dan HttpClient saat aplikasi mulai.
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()],
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 60: Buat dan jalankan dengan CLI
Buat komponen dan layanan, lalu sajikan aplikasi.
cd web
ng generate component products/product-list
ng generate service products/product
ng serve --open
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 9: Layanan, HTTP, dan Perutean
Layanan menyimpan logika dan berkomunikasi dengan API, sehingga komponen tetap kecil. Router memetakan URL ke komponen, penjaga melindungi halaman, dan pemuatan malas menjaga unduhan awal tetap kecil.
Layanan yang dapat diinjeksi dan HttpClient
Tandai layanan dengan providedIn root agar Angular membuat satu instans untuk seluruh aplikasi. Injeksikan HttpClient dan kembalikan Observable yang bertipe sesuai antarmuka respons, sehingga kompilator memeriksa kolom yang Anda gunakan.
Rute dan parameter
Definisikan rute sebagai larik pasangan jalur dan komponen. Baca parameter dari ActivatedRoute, dan tambahkan rute wildcard di akhir yang menampilkan halaman tidak ditemukan.
Penjaga dan pemuatan malas
Penjaga CanActivate mengembalikan true atau pengalihan. Rute malas memakai loadComponent dengan dynamic import, sehingga kode admin hanya diunduh saat pengguna membukanya.
Contoh 61: Layanan yang dapat diinjeksi
Bagikan status antarkomponen dengan satu instans.
import { Injectable } from "@angular/core";
@Injectable({ providedIn: "root" })
export class CartService {
private items: { id: number; qty: number }[] = [];
add(id: number) {
const found = this.items.find((i) => i.id === id);
if (found) found.qty++;
else this.items.push({ id, qty: 1 });
}
count() {
return this.items.reduce((sum, i) => sum + i.qty, 0);
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 62: Panggil API dengan HttpClient
Permintaan bertipe yang mengembalikan Observable.
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
export interface Product { id: number; name: string; price: number; stock: number; }
@Injectable({ providedIn: "root" })
export class ProductService {
private http = inject(HttpClient);
list(): Observable<Product[]> {
return this.http.get<Product[]>("/api/products");
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 63: Definisikan rute
Petakan URL ke komponen, dengan halaman cadangan.
import { Routes } from "@angular/router";
export const routes: Routes = [
{ path: "", pathMatch: "full", redirectTo: "products" },
{ path: "products", component: ProductListComponent },
{ path: "products/:id", component: ProductDetailComponent },
{ path: "**", component: NotFoundComponent },
];
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 64: Penjaga rute
Arahkan pengguna yang belum login ke halaman masuk.
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "./auth.service";
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn() || inject(Router).createUrlTree(["/login"]);
};
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 65: Baca parameter rute
Muat satu produk dari URL.
import { Component, inject } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
@Component({ selector: "app-product-detail", standalone: true, template: "" })
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
readonly id = Number(this.route.snapshot.paramMap.get("id"));
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 66: Muat fitur secara malas
Unduh kode admin hanya saat dibutuhkan.
export const routes: Routes = [
{
path: "admin",
canActivate: [authGuard],
loadComponent: () => import("./admin/admin.component").then((m) => m.AdminComponent),
},
];
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 10: Formulir dan Validasi
Formulir mengumpulkan data yang akan disimpan API. Reactive forms menyimpan model di TypeScript, sehingga validasi dan pengujian menjadi lurus dan templat tetap sederhana.
Reactive forms
Bangun formulir dengan FormBuilder, jelaskan setiap kolom dengan nilai bawaan dan validatornya, lalu ikat templat dengan formGroup dan formControlName. Nilai formulir bertipe, sehingga kode pengiriman diperiksa oleh kompilator.
Validator kustom dan asinkron
Validator adalah fungsi yang mengembalikan null saat nilai valid, atau objek yang menjelaskan galat. Tulis validator untuk aturan yang tidak dicakup validator bawaan, misalnya nilai yang hanya berisi spasi.
Menampilkan galat dan mengirim data
Tampilkan pesan hanya setelah pengguna menyentuh kolom, agar formulir tidak mulai dalam keadaan galat. Saat server mengembalikan galat validasi, tampilkan di dekat formulir, bukan kotak peringatan umum.
Contoh 67: Formulir reaktif
Bangun formulir di TypeScript dengan validator.
import { Component, inject } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
@Component({ selector: "app-product-form", standalone: true, imports: [ReactiveFormsModule], templateUrl: "./product-form.html" })
export class ProductFormComponent {
private fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
name: ["", [Validators.required, Validators.minLength(2)]],
price: [0, [Validators.required, Validators.min(0)]],
stock: [0, [Validators.min(0)]],
});
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 68: Templat formulir
Ikat kolom masukan ke model formulir.
<form [formGroup]="form" (ngSubmit)="save()">
<label>Name <input formControlName="name"></label>
<label>Price <input type="number" formControlName="price"></label>
<label>Stock <input type="number" formControlName="stock"></label>
<button type="submit" [disabled]="form.invalid">Save</button>
</form>
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 69: Validator kustom
Tolak nilai yang hanya berisi spasi.
import { AbstractControl, ValidationErrors } from "@angular/forms";
export function noSpacesOnly(control: AbstractControl): ValidationErrors | null {
const value = String(control.value ?? "");
return value.trim().length === 0 && value.length > 0 ? { spacesOnly: true } : null;
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 70: Tampilkan pesan validasi
Tampilkan galat setelah pengguna menyentuh kolom.
@if (form.controls.name.touched && form.controls.name.invalid) {
<p class="error">
@if (form.controls.name.errors?.['required']) { Name is required. }
@if (form.controls.name.errors?.['minlength']) { Name must be at least 2 characters. }
</p>
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 71: Kirim formulir ke API
Kirim data dan pindah halaman jika berhasil.
save() {
if (this.form.invalid) return;
this.http.post("/api/products", this.form.getRawValue()).subscribe({
next: () => this.router.navigate(["/products"]),
error: (err) => (this.serverError = err.error?.errors ?? ["Could not save"]),
});
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 72: Tampilkan galat dari server
Tampilkan pesan validasi yang dikembalikan API.
// After a 400 response, the API sends { errors: ["price must be a number >= 0"] }
serverError: string[] = [];
// in the template
// @for (e of serverError; track e) { <p class="error">{{ e }}</p> }
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 11: Status, Observable, dan Interseptor
Data berubah seiring waktu, dan banyak permintaan harus dikoordinasikan. Observable dan signal menjelaskan perubahan itu, sementara interseptor menambahkan perilaku lintas bagian seperti token dan pencatatan galat ke setiap permintaan.
Observable dan pipa async
Observable memancarkan nilai dari waktu ke waktu. Pipa async berlangganan di templat dan berhenti berlangganan secara otomatis, sehingga menghindari kebocoran memori dan kode langganan manual.
Signal untuk status lokal
Signal menyimpan nilai yang memberi tahu templat saat nilainya berubah. computed menurunkan nilai dari signal, dan effect menjalankan kode saat nilainya berubah. Signal cocok untuk status sederhana seperti penghitung atau total keranjang.
Interseptor
Interseptor HTTP melihat setiap permintaan dan respons. Gunakan satu untuk menambahkan header Authorization, dan yang lain untuk mencatat kegagalan atau mengalihkan ke halaman login saat token kedaluwarsa.
Contoh 73: Observable dan pipa async
Tampilkan daftar tanpa berlangganan secara manual.
@Component({
selector: "app-products",
standalone: true,
imports: [AsyncPipe],
template: `
@for (p of products$ | async; track p.id) {
<div>{{ p.name }}</div>
}`,
})
export class ProductsComponent {
products$ = inject(ProductService).list();
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 74: Interseptor yang menambahkan token
Lampirkan header Authorization pada setiap permintaan.
import { HttpInterceptorFn } from "@angular/common/http";
import { inject } from "@angular/core";
import { AuthService } from "./auth.service";
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 75: Interseptor galat global
Catat kegagalan API di satu tempat.
import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http";
import { catchError, throwError } from "rxjs";
export const errorInterceptor: HttpInterceptorFn = (req, next) =>
next(req).pipe(
catchError((err: HttpErrorResponse) => {
console.error("API error", err.status, req.url);
return throwError(() => err);
})
);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 76: Signal untuk status lokal
Lacak satu nilai dan turunkan nilai lain darinya.
import { Component, computed, signal } from "@angular/core";
@Component({ selector: "app-counter", standalone: true, template: "" })
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() {
this.count.update((n) => n + 1);
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 77: Store kecil dengan signal
Simpan status keranjang di satu layanan.
import { Injectable, computed, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class CartStore {
private readonly lines = signal<{ id: number; qty: number }[]>([]);
readonly totalQty = computed(() => this.lines().reduce((s, l) => s + l.qty, 0));
add(id: number) {
this.lines.update((ls) => [...ls, { id, qty: 1 }]);
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 78: Berhenti berlangganan otomatis
Hentikan aliran saat komponen dihancurkan.
import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export class ClockComponent {
constructor() {
interval(1000).pipe(takeUntilDestroyed()).subscribe(() => this.tick());
}
tick() {}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 12: Integrasi Full-Stack
Menghubungkan bagian-bagian adalah titik ketika banyak proyek bermasalah. Proxy saat pengembangan, berkas environment untuk setiap deployment, dan tipe bersama menjaga front end dan API tetap selaras.
Proxy pengembangan
Teruskan /api dari server pengembangan Angular ke server Node.js, sehingga browser melihat satu origin dan tidak diperlukan konfigurasi CORS selama pengembangan. Mulai ulang ng serve setelah mengubah berkas proxy.
Berkas environment
Berkas environment Angular memberi setiap build pengaturannya sendiri, seperti alamat API. Jauhkan alamat produksi dari sumber jika bersifat rahasia, dan baca dari konfigurasi build sebagai gantinya.
Tipe bersama
Jelaskan bentuk Product sekali di folder bersama dan impor di API maupun front end. Kolom yang diganti namanya di satu sisi akan muncul sebagai galat kompilator di sisi lain, bukan bug saat runtime.
Contoh 79: Proksi panggilan API saat pengembangan
Teruskan /api ke server Node.js.
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 80: Berkas lingkungan
Gunakan alamat API yang berbeda di produksi.
// src/environments/environment.ts
export const environment = {
production: false,
apiBaseUrl: "/api",
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiBaseUrl: "https://api.shop.example.com",
};
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 81: Bangun URL API
Gabungkan alamat dasar dan jalur di satu tempat.
import { environment } from "../environments/environment";
export const api = (path: string) => `${environment.apiBaseUrl}${path}`;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 82: Jalankan API dan front end
Mulai kedua server di dua terminal.
# terminal 1
cd api && npm run dev
# terminal 2
cd web && ng serve --proxy-config proxy.conf.json
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 83: Bagikan tipe antara front end dan back end
Jaga bentuk Product tetap sama di kedua sisi.
// shared/product.ts (used by both web and api through a path alias)
export interface Product {
id: number;
name: string;
price: number;
stock: number;
}
export type CreateProduct = Omit<Product, "id">;
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 84: Periksa kesehatan API dari Angular
Tampilkan apakah back end dapat dijangkau.
export class AppComponent {
private http = inject(HttpClient);
apiOk = signal<boolean | null>(null);
constructor() {
this.http.get<{ status: string }>("/api/health").subscribe({
next: (r) => this.apiOk.set(r.status === "ok"),
error: () => this.apiOk.set(false),
});
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 13: Menguji Setiap Lapisan
Pengujian menangkap kesalahan sebelum pengguna menemukannya. Uji fungsi murni, rute API, komponen dan layanan Angular, dan akhirnya seluruh tumpukan dengan basis data yang terpisah.
Pengujian API
Supertest mengirim permintaan ke aplikasi Express tanpa membuka port sungguhan, dan Jest atau node:test memeriksa hasilnya. Uji jalur sukses dan setidaknya satu kegagalan, seperti token yang hilang atau harga yang tidak valid.
Pengujian Angular
TestBed membuat komponen beserta dependensinya. HttpTestingController menjawab panggilan HTTP dengan data palsu, sehingga komponen dapat diuji tanpa menjalankan API.
Integrasi dan cakupan
Jalankan pengujian integrasi terhadap basis data uji yang terpisah dan di-reset sebelum setiap eksekusi, sehingga pengujian tidak pernah mengubah data sungguhan. Cakupan menunjukkan kode mana yang tidak tersentuh pengujian, ini adalah titik awal, bukan jaminan.
Contoh 85: Uji rute Express
Panggil rute dengan Supertest dan periksa responsnya.
const request = require("supertest");
const app = require("../app");
test("GET /api/health returns ok", async () => {
const res = await request(app).get("/api/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe("ok");
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 86: Uji fungsi murni
Gunakan pelari uji bawaan node:test.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { validateProduct } = require("../validate");
test("rejects a short name", () => {
assert.equal(validateProduct({ name: "A", price: 10 }).length, 1);
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 87: Uji komponen
Buat komponen dengan TestBed.
import { TestBed } from "@angular/core/testing";
import { ProductListComponent } from "./product-list.component";
it("creates the component", () => {
TestBed.configureTestingModule({ imports: [ProductListComponent] });
const fixture = TestBed.createComponent(ProductListComponent);
expect(fixture.componentInstance).toBeTruthy();
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 88: Tiru HTTP dengan HttpTestingController
Jawab permintaan tanpa server sungguhan.
import { TestBed } from "@angular/core/testing";
import { provideHttpClient } from "@angular/common/http";
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
import { ProductService } from "./product.service";
it("loads products", () => {
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
const service = TestBed.inject(ProductService);
const http = TestBed.inject(HttpTestingController);
service.list().subscribe((items) => expect(items.length).toBe(1));
http.expectOne("/api/products").flush([{ id: 1, name: "Mouse", price: 350, stock: 3 }]);
http.verify();
});
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 89: Uji integrasi dengan basis data uji
Jalankan API terhadap basis data terpisah.
# create a separate test database
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS shop_test"
DB_NAME=shop_test npm test -- --runInBand
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 90: Laporan cakupan
Ukur seberapa banyak kode yang dijangkau pengujian.
cd api && npx jest --coverage
cd ../web && ng test --code-coverage --watch=false
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 14: Docker dan Deployment
Kontainer mengemas setiap bagian beserta dependensi yang tepat. Docker Compose menjalankan basis data, API, dan server web bersama-sama, dan image yang sama dapat berjalan di laptop, server uji, atau layanan cloud.
Image untuk API dan aplikasi web
Gunakan build multi-tahap: instal dan bangun di satu tahap, lalu salin hanya yang dibutuhkan image akhir. Image API berjalan sebagai pengguna non-root, dan image Angular hanya berisi berkas hasil build yang disajikan oleh Nginx.
Compose dan startup basis data
Compose menjalankan MySQL lebih dulu dan menunggu pemeriksaan kesehatannya sebelum memulai API. Letakkan skrip skema di folder init, yang hanya dijalankan MySQL saat volume datanya kosong.
Proksi terbalik
Nginx menyajikan berkas Angular, meneruskan /api ke kontainer API, dan menangani aturan try_files yang membuat rute sisi klien tetap berfungsi saat halaman dimuat ulang.
Contoh 91: Dockerfile untuk API
Image produksi kecil yang berjalan sebagai pengguna non-root.
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 92: Dockerfile untuk aplikasi Angular
Bangun aplikasi dan sajikan dengan Nginx.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx ng build --configuration production
FROM nginx:1.27-alpine
COPY --from=build /app/dist/web/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 93: Compose dengan MySQL, API, dan web
Jalankan seluruh tumpukan dengan satu perintah.
services:
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: shop
MYSQL_USER: shop
MYSQL_PASSWORD: <strong-password>
MYSQL_ROOT_PASSWORD: <root-password>
volumes:
- db-data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 20
api:
build: ./api
env_file: ./api/.env
depends_on:
db:
condition: service_healthy
web:
build: ./web
ports:
- "80:80"
depends_on:
- api
volumes:
db-data:
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 94: Jalankan skema saat pertama kali mulai
Buat tabel otomatis di volume baru.
# db/init/01-schema.sql is run once when the data volume is empty
services:
db:
volumes:
- ./db/init:/docker-entrypoint-initdb.d:ro
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Topik 15: Operasi Produksi
Sebuah rilis belum selesai hanya karena pernah berjalan sekali. Produksi membutuhkan konfigurasi yang aman, log terstruktur, cadangan, pipeline integrasi berkelanjutan, dan daftar periksa yang menghentikan rilis yang buruk.
Konfigurasi yang aman
Tolak startup saat kunci penandatanganan tidak ada, percayai header proksi hanya saat ada proksi di depan, dan nonaktifkan header framework yang mengungkap perangkat lunak server.
Log dan cadangan
Tulis log JSON terstruktur yang menyertakan ID permintaan, sehingga satu masalah dapat dilacak di seluruh tumpukan. Cadangkan basis data dengan mysqldump dan --single-transaction, dan uji pemulihannya di mesin lain.
Pipeline dan daftar periksa rilis
Alur kerja CI menjalankan pengujian API, pengujian Angular, dan build produksi pada setiap dorongan. Skrip rilis memeriksa endpoint kesehatan dan kontainer sebelum versi baru diumumkan.
Contoh 95: Pengaturan produksi di API
Tolak mulai tanpa kunci rahasia.
const isProd = process.env.NODE_ENV === "production";
if (isProd && !process.env.JWT_SECRET) {
throw new Error("JWT_SECRET must be set in production");
}
app.set("trust proxy", 1);
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 96: Proksi balik dengan Nginx
Sajikan aplikasi dan teruskan /api ke API.
server {
listen 80;
root /usr/share/nginx/html;
location /api/ {
proxy_pass http://api:3000;
proxy_set_header Host $host;
}
location / {
try_files $uri /index.html;
}
}
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 97: Log terstruktur dengan pino
Tulis log JSON yang bisa dicari alat.
const pino = require("pino");
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(require("pino-http")({ logger }));
logger.info({ port: 3000 }, "API started");
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 98: Cadangkan basis data MySQL
Buat dump yang konsisten lalu kompres.
mysqldump -h db -u shop -p shop --single-transaction > backup-$(date +%F).sql
gzip backup-$(date +%F).sql
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 99: Alur kerja CI di GitHub Actions
Uji dan bangun setiap dorongan ke main.
name: ci
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci --prefix api
- run: npm test --prefix api
- run: npm ci --prefix web
- run: npx ng test --watch=false --prefix web
- run: npx ng build --configuration production --prefix web
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Contoh 100: Skrip daftar periksa rilis
Hentikan rilis jika ada pemeriksaan yang gagal.
#!/usr/bin/env bash
set -euo pipefail
curl -fsS https://shop.example.com/api/health | grep -q '"status":"ok"'
docker compose ps --format json | grep -q '"Health":"healthy"'
gzip -t backup-latest.sql.gz
echo "release checks passed"
*Contoh ini tidak dijalankan di sini karena MySQL dan Angular CLI tidak terpasang di komputer yang menyiapkan artikel ini.*
Lab: Membangun, Menguji, dan Merilis Toko Kecil
Satukan artikel ini dalam satu proyek: API dengan login dan rute produk, skema MySQL dengan lapisan kueri yang aman, halaman produk Angular dengan reactive form, pengujian untuk setiap lapisan, dan tumpukan Docker Compose.
1. Buat folder api dan web, pasang Express, mysql2, dotenv, dan bcrypt di API, lalu buat aplikasi Angular dengan routing.
2. Buat tabel products dan users di MySQL dengan pengguna yang memiliki hak terbatas, lalu isi beberapa produk.
3. Tulis rute produk dengan placeholder, validasi, dan penangan galat terpusat, lalu uji dengan curl atau Supertest.
4. Tambahkan registrasi dan login dengan kata sandi ter-hash dan JWT, lalu lindungi rute pesanan dengan middleware.
5. Bangun daftar produk Angular dengan HttpClient, rute detail produk, dan reactive form untuk menambah produk.
6. Tambahkan berkas proxy dan jalankan API serta aplikasi web bersama-sama, lalu uji seluruh alur di browser.
7. Tulis satu pengujian API dan satu pengujian komponen, jalankan keduanya, lalu mulai seluruh tumpukan dengan docker compose up dan jalankan daftar periksa rilis dari contoh 100.
Langkah berikutnya
Setelah lab, baca dokumentasi resmi Angular, Express, dan MySQL, tambahkan paginasi dan pencarian ke toko, dan bangun fitur kedua dari awal sampai akhir tanpa menyalin kode. Simpan contoh di repositori dan jalankan ulang pengujian setelah setiap peningkatan dependensi, karena framework berubah antarversi utama.
Ringkasan
Aplikasi full-stack menghubungkan tiga bagian yang harus sesuai satu sama lain. Anda telah mempelajari penyiapan proyek dan alat, menulis kode Node.js dan Express, merancang tabel MySQL dan kueri yang aman, menghubungkan API dengan pool dan transaksi, memvalidasi input dan menangani galat, melindungi rute dengan kata sandi ter-hash dan token, membangun komponen, layanan, rute, formulir, dan interseptor Angular, menghubungkan front end ke API, menguji setiap lapisan, mengemas sistem dengan Docker, serta menyiapkannya untuk produksi.