commit cd72a98d70f02838954a2747959fdc9f7aace27e Author: Benaya Nathanael Yeroham Date: Wed Sep 2 16:58:57 2026 +0700 Add OpenAPI products API diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9dbdec6 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Copy file ini menjadi .env dan isi dengan connection string Neon kamu +# Dapatkan connection string dari dashboard Neon: Overview → Connection Details + +DATABASE_URL=postgresql://username:password@ep-xxx-xxx-123456.us-east-2.aws.neon.tech/neondb?sslmode=require diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cff5543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +__pycache__/ +*.pyc diff --git a/app.py b/app.py new file mode 100644 index 0000000..7f4078f --- /dev/null +++ b/app.py @@ -0,0 +1,121 @@ +""" +Backend Flask — Products API +""" +import os +import psycopg +from flask import Flask, jsonify + +app = Flask(__name__) + + +def get_db_connection(): + """Membuat koneksi ke database Neon PostgreSQL.""" + return psycopg.connect(os.environ["DATABASE_URL"]) + + +@app.get("/") +def home(): + """Cek apakah server hidup.""" + return jsonify({"status": "ok", "message": "Products API is running"}) + + +@app.get("/products") +def get_products(): + """Mengambil semua produk dari database.""" + conn = get_db_connection() + cur = conn.cursor() + cur.execute("SELECT id, name, price, stock FROM products ORDER BY id") + rows = cur.fetchall() + cur.close() + conn.close() + + products = [ + {"id": r[0], "name": r[1], "price": float(r[2]), "stock": r[3]} + for r in rows + ] + return jsonify(products) + + +@app.get("/products/") +def get_product_by_id(product_id): + """Mengambil satu produk berdasarkan ID.""" + conn = get_db_connection() + cur = conn.cursor() + cur.execute( + "SELECT id, name, price, stock FROM products WHERE id = %s", + (product_id,), + ) + row = cur.fetchone() + cur.close() + conn.close() + + if row is None: + return jsonify({"error": "Product not found"}), 404 + + product = { + "id": row[0], + "name": row[1], + "price": float(row[2]), + "stock": row[3], + } + return jsonify(product) + + +@app.post("/products") +def create_product(): + """Menambah produk baru ke database.""" + from flask import request + + if not request.is_json: + return jsonify({"error": "Request must be JSON"}), 400 + + data = request.get_json() + + for field in ("name", "price", "stock"): + if field not in data: + return jsonify({"error": f"{field} is required"}), 400 + + conn = get_db_connection() + cur = conn.cursor() + try: + cur.execute( + "INSERT INTO products (name, price, stock) VALUES (%s, %s, %s) " + "RETURNING id, name, price, stock", + (data["name"], data["price"], data["stock"]), + ) + row = cur.fetchone() + conn.commit() + except Exception: + conn.rollback() + return jsonify({"error": "Invalid input"}), 400 + finally: + cur.close() + conn.close() + + product = { + "id": row[0], + "name": row[1], + "price": float(row[2]), + "stock": row[3], + } + return jsonify(product), 201 + + +@app.delete("/products/") +def delete_product(product_id): + """Menghapus satu produk berdasarkan ID.""" + conn = get_db_connection() + cur = conn.cursor() + + cur.execute("SELECT id FROM products WHERE id = %s", (product_id,)) + if cur.fetchone() is None: + cur.close() + conn.close() + return jsonify({"error": "Product not found"}), 404 + + cur.execute("DELETE FROM products WHERE id = %s", (product_id,)) + conn.commit() + cur.close() + conn.close() + + return "", 204 diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..02baa34 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,178 @@ +openapi: 3.0.3 +info: + title: Products API + version: 1.0.0 + description: API untuk mengelola data products pada database Neon + +servers: + - url: https://your-api-url.vercel.app + description: Vercel (sesuaikan dengan URL deploy kamu) + +paths: + /products: + get: + summary: Get all products + operationId: getProducts + description: Mengambil semua produk dari database + tags: + - Products + responses: + '200': + description: Daftar semua produk + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Product' + + post: + summary: Create a new product + operationId: createProduct + description: Menambah produk baru ke database + tags: + - Products + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProductInput' + responses: + '201': + description: Produk berhasil dibuat + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + '400': + description: Input tidak valid + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /products/{id}: + get: + summary: Get product by ID + operationId: getProductById + description: Mengambil satu produk berdasarkan ID + tags: + - Products + parameters: + - name: id + in: path + required: true + description: ID produk + schema: + type: integer + responses: + '200': + description: Produk ditemukan + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + '400': + description: ID tidak valid + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Produk tidak ditemukan + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + summary: Delete a product + operationId: deleteProduct + description: Menghapus satu produk berdasarkan ID + tags: + - Products + parameters: + - name: id + in: path + required: true + description: ID produk + schema: + type: integer + responses: + '204': + description: Produk berhasil dihapus + '400': + description: ID tidak valid + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Produk tidak ditemukan + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + Product: + type: object + description: Representasi satu produk dari database + properties: + id: + type: integer + description: ID unik produk, auto-generated oleh database + example: 1 + name: + type: string + description: Nama produk + maxLength: 100 + example: "Laptop" + price: + type: number + description: Harga produk + example: 8500000.00 + stock: + type: integer + description: Jumlah stok produk + example: 10 + required: + - id + - name + - price + - stock + + ProductInput: + type: object + description: Schema untuk request body saat menambahkan produk baru + properties: + name: + type: string + description: Nama produk + maxLength: 100 + example: "Monitor" + price: + type: number + description: Harga produk + example: 1500000.00 + stock: + type: integer + description: Jumlah stok produk + example: 5 + required: + - name + - price + - stock + + Error: + type: object + description: Schema untuk response error + properties: + error: + type: string + description: Pesan error + example: "Product not found" + required: + - error diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8ee71f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask>=3.0.0 +psycopg>=3.1.0 +python-dotenv>=1.0.0