Add OpenAPI products API
This commit is contained in:
commit
cd72a98d70
4
.env.example
Normal file
4
.env.example
Normal file
@ -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
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
121
app.py
Normal file
121
app.py
Normal file
@ -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/<int:product_id>")
|
||||||
|
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/<int:product_id>")
|
||||||
|
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
|
||||||
178
openapi.yaml
Normal file
178
openapi.yaml
Normal file
@ -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
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Flask>=3.0.0
|
||||||
|
psycopg>=3.1.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
Loading…
x
Reference in New Issue
Block a user