CRUD Rest API di Node.js memakai Express dan Sequelize (MySQL)


Assalamualaikum Wr.Wb...

Berikut contoh lengkap REST API CRUD Product di Node.js dengan fitur:

- CRUD Product List data pagination
- Upload image & PDF
- Keamanan terhadap RCE, LFI, XSS, SQLi, file tampering / webshell
- Validasi & sanitasi input
- Proteksi upload file

Contoh ini memakai Express + Sequelize (MySQL).

1. Instalasi Dependency

npm init -y
npm install express sequelize sqlite3 multer helmet cors express-validator
npm install xss sanitize-filename file-type
npm install dotenv

2. Struktur Folder

project/
│── app.js
│── .env
│── database.js
│── package.json
├── models/
│   └── Product.js
├── controllers/
│   └── product.controller.js
├── routes/
│   └── product.routes.js
├── middlewares/
│   ├── upload.middleware.js
│   └── security.middleware.js
├── uploads/
│   ├── images/
│   └── files/
└── database/
    └── product.sql

2. File .env

APP_PORT=3000

DB_HOST=localhost
DB_PORT=3306
DB_NAME=product_db
DB_USER=root
DB_PASS=passwordkamu

MAX_FILE_SIZE=2097152

3. Struktur Database MySQL
SQL Schema (database/product.sql)

CREATE DATABASE IF NOT EXISTS product_db;
USE product_db;

CREATE TABLE products (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    price DECIMAL(15,2) NOT NULL,
    image VARCHAR(255),
    document VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE INDEX idx_products_name ON products(name);

Aman dari serangan:

- SQL Injection → ORM
- Integer overflow → BIGINT
- File abuse → path tidak disimpan absolut

4. Konfigurasi Sequelize MySQL (database.js)

const { Sequelize } = require("sequelize");
require("dotenv").config();

const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASS,
  {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    dialect: "mysql",
    logging: false,
    dialectOptions: {
      multipleStatements: false
    },
    pool: {
      max: 10,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  }
);

module.exports = sequelize;

Keamanan config database:

- multipleStatements: false → cegah SQLi chaining
- Connection pool stabil

5. Buat file Model Sequelize (models/Product.js)

const { DataTypes } = require("sequelize");
const sequelize = require("../database");

const Product = sequelize.define(
  "Product",
  {
    id: {
      type: DataTypes.BIGINT.UNSIGNED,
      autoIncrement: true,
      primaryKey: true
    },
    name: {
      type: DataTypes.STRING(255),
      allowNull: false
    },
    description: {
      type: DataTypes.TEXT
    },
    price: {
      type: DataTypes.DECIMAL(15,2),
      allowNull: false
    },
    image: {
      type: DataTypes.STRING(255)
    },
    document: {
      type: DataTypes.STRING(255)
    }
  },
  {
    tableName: "products",
    underscored: true,
    timestamps: true
  }
);

module.exports = Product;

6. Buat Middleware Keamanan Global di folder
(middlewares/security.middleware.js)

const helmet = require("helmet");
const xss = require("xss");

module.exports = (app) => {
  app.use(helmet());

  app.use((req, res, next) => {
    ["body", "query", "params"].forEach((key) => {
      if (req[key]) {
        for (const prop in req[key]) {
          if (typeof req[key][prop] === "string") {
            req[key][prop] = xss(req[key][prop]);
          }
        }
      }
    });
    next();
  });
};

Melindungi dari:

- XSS
- Header injection
- CSP bypass

7. Buat Upload File di (middlewares/upload.middleware.js)

const multer = require("multer");
const path = require("path");
const sanitize = require("sanitize-filename");

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    if (file.mimetype.startsWith("image/")) {
      cb(null, "uploads/images");
    } else if (file.mimetype === "application/pdf") {
      cb(null, "uploads/files");
    } else {
      cb(new Error("Invalid file"));
    }
  },
  filename: (req, file, cb) => {
    const safeName = sanitize(file.originalname)
      .replace(/\s+/g, "_")
      .toLowerCase();
    cb(null, Date.now() + "-" + safeName);
  }
});

const fileFilter = (req, file, cb) => {
  const allowed = [
    "image/png",
    "image/jpeg",
    "application/pdf"
  ];

  if (!allowed.includes(file.mimetype)) {
    return cb(new Error("File type forbidden"), false);
  }

  cb(null, true);
};

module.exports = multer({
  storage,
  fileFilter,
  limits: {
    fileSize: parseInt(process.env.MAX_FILE_SIZE)
  }
});

Proteksi Upload:

- Tidak bisa upload ekstensi file contoh .js
- MIME whitelist
- Filename sanitized
- Size limit
- Tidak bisa path traversal

8. Buat Controller CRUD dan list Pagination
di controllers/product.controller.js

const Product = require("../models/Product");
const { validationResult } = require("express-validator");

exports.create = async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json(errors.array());
  }

  const product = await Product.create({
    name: req.body.name,
    description: req.body.description,
    price: req.body.price,
    image: req.files?.image?.[0]?.filename || null,
    document: req.files?.document?.[0]?.filename || null
  });

  res.json(product);
};

exports.list = async (req, res) => {
  const page = Math.max(parseInt(req.query.page) || 1, 1);
  const limit = Math.min(parseInt(req.query.limit) || 10, 50);
  const offset = (page - 1) * limit;

  const result = await Product.findAndCountAll({
    limit,
    offset,
    order: [["id", "DESC"]]
  });

  res.json({
    total: result.count,
    page,
    pages: Math.ceil(result.count / limit),
    data: result.rows
  });
};

exports.detail = async (req, res) => {
  const product = await Product.findByPk(req.params.id);
  if (!product) {
    return res.status(404).json({ message: "Not found" });
  }
  res.json(product);
};

exports.update = async (req, res) => {
  const product = await Product.findByPk(req.params.id);
  if (!product) {
    return res.status(404).json({ message: "Not found" });
  }

  await product.update(req.body);
  res.json(product);
};

exports.delete = async (req, res) => {
  const product = await Product.findByPk(req.params.id);
  if (!product) {
    return res.status(404).json({ message: "Not found" });
  }

  await product.destroy();
  res.json({ message: "Deleted" });
};

9. Buat Routes di (routes/product.routes.js)

const express = require("express");
const { body } = require("express-validator");
const upload = require("../middlewares/upload.middleware");
const controller = require("../controllers/product.controller");

const router = express.Router();

router.post(
  "/",
  upload.fields([
    { name: "image", maxCount: 1 },
    { name: "document", maxCount: 1 }
  ]),
  [
    body("name").isString().trim().notEmpty(),
    body("price").isFloat()
  ],
  controller.create
);

router.get("/", controller.list);
router.get("/:id", controller.detail);
router.put("/:id", controller.update);
router.delete("/:id", controller.delete);

module.exports = router;

10. Buat App Utama file (app.js)

require("dotenv").config();
const express = require("express");
const cors = require("cors");
const sequelize = require("./database");
const security = require("./middlewares/security.middleware");
const productRoutes = require("./routes/product.routes");

const app = express();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

security(app);

app.use("/uploads", express.static("uploads"));

app.use("/api/products", productRoutes);

// ini untuk Anti LFI & path traversal
app.use((req, res, next) => {
  if (req.url.includes("..")) {
    return res.status(403).json({ message: "Forbidden" });
  }
  next();
});

sequelize.authenticate().then(() => {
  console.log("DB Connected");
  sequelize.sync();
});

app.listen(process.env.APP_PORT, () =>
  console.log("Server running on port " + process.env.APP_PORT)
);

oke lanjut ke contoh hasil rest api dengan endpoint nya

BASE URL

http://localhost:3000/api/products

CREATE PRODUCT

POST /

Membuat produk baru dengan upload image & PDF

Endpoint

POST /api/products

di Headers

Content-Type multipart/form-data

Response Success (200)

{
  "id": 1,
  "name": "Produk A",
  "description": "Deskripsi produk",
  "price": "10000.00",
  "image": "1700000000-image.jpg",
  "document": "1700000000-manual.pdf",
  "created_at": "2026-01-01T10:00:00.000Z",
  "updated_at": "2026-01-01T10:00:00.000Z"
}

Response Error 422 – Validation Error

[
  {
    "msg": "Invalid value",
    "param": "price",
    "location": "body"
  }
]

400 – File Not Allowed

{
  "message": "File type forbidden"
}

LIST PRODUCT (Pagination)

GET /

Endpoint

GET /api/products

Contoh Request

GET /api/products?page=1&limit=10

Response Success (200)

{
  "total": 25,
  "page": 1,
  "pages": 3,
  "data": [
    {
      "id": 5,
      "name": "Produk A",
      "price": "10000.00",
      "image": "1700000000-image.jpg",
      "document": "1700000000-manual.pdf"
    }
  ]
}

DETAIL PRODUCT

GET /:id

Endpoint

GET /api/products/{id}

Contoh Request

GET /api/products/1

Response Success (200)

{
  "id": 1,
  "name": "Produk A",
  "description": "Deskripsi produk",
  "price": "10000.00",
  "image": "1700000000-image.jpg",
  "document": "1700000000-manual.pdf",
  "created_at": "2026-01-01T10:00:00.000Z",
  "updated_at": "2026-01-01T10:00:00.000Z"
}

Response Error 404 – Not Found

{
  "message": "Not found"
}

UPDATE PRODUCT

PUT /:id

Endpoint

PUT /api/products/{id}

Headers

Content-Type: application/json

Contoh Request

{
  "name": "Produk Update",
  "price": 20000
}

Response Success (200)

{
  "id": 1,
  "name": "Produk Update",
  "price": "20000.00"
}

DELETE PRODUCT

DELETE /:id

Endpoint

DELETE /api/products/{id}

Contoh Request

DELETE /api/products/1

Response Success (200)

{
  "message": "Deleted"
}

Response Error

{
  "message": "Not found"
}

oke mungkin itu saja sekian dan semoga bermanfaat....

Wasalamualaikum Wr.Wb...

Post a Comment

0 Comments