رفتن به محتوا
پایتون از صفرپایتون در کار واقعیدرس ۹ از ۱۰

یک API کوچک

با پایتون یک سرویس وب بساز: یادآوری HTTP (متد، مسیر، کد وضعیت، JSON)، نصب Flask در venv، مسیرهای GET و POST، پارامتر در آدرس و query string، کدهای 200 و 201 و 400 و 404 و 409، آزمایش با curl، فراخوانی API با requests، و فرق سرور توسعه با production.

  • حدود ۲۰ دقیقه مطالعه
  • ۷ مثال اجراشده
  • ۳ تمرین با جواب
  • مقدماتی تا متوسط
  • به‌روزرسانی: ۲۹ مهر ۱۴۰۵

توی این درس یاد می‌گیری با پایتون یک API بسازی: برنامه‌ای که روی یک پورت گوش می‌دهد، درخواست HTTP می‌گیرد و جواب JSON برمی‌گرداند؛ همان چیزی که پشت هر اپ موبایل و هر سایت مدرن هست. با کتابخانه‌ی Flask مسیرهای GET (خواندن) و POST (ساختن) می‌نویسی، ورودی را اعتبارسنجی می‌کنی و کد وضعیت درست (۲۰۰، ۲۰۱، ۴۰۰، ۴۰۴، ۴۰۹) برمی‌گردانی. API را با curl از ترمینال آزمایش می‌کنی و با کتابخانه‌ی requests از یک برنامه‌ی پایتون دیگر صدایش می‌زنی. در آخر می‌فهمی چرا سرور توسعه‌ی Flask برای production نیست.

مسئله: داده‌ی برنامه‌ی تو فقط روی کامپیوتر خودت است

Section titled “مسئله: داده‌ی برنامه‌ی تو فقط روی کامپیوتر خودت است”

برنامه‌های درس‌های قبل روی ترمینال تو اجرا می‌شدند و فقط تو نتیجه‌شان را می‌دیدی. ولی وقتی داشبورد تیم می‌خواهد وضعیت سرورها را نشان بدهد، یا اپ موبایل فهرست سفارش‌ها را لازم دارد، یا یک اسکریپت دیگر باید به برنامه‌ی تو چیزی بگوید، باید برنامه‌ات را از راه شبکه در دسترس بگذاری. زبان مشترک این کار HTTP و JSON است.

API مثل پیشخوان سفارش یک رستوران است. مشتری (کلاینت) لازم نیست بداند آشپزخانه چطور کار می‌کند؛ فقط از منو (مسیرهای API) انتخاب می‌کند: «یک فهرست سرور بده» (GET /servers) یا «این سرور جدید را ثبت کن» (POST /servers). پیشخوان جواب می‌دهد: «بفرمایید» (۲۰۰)، «ثبت شد» (۲۰۱)، «سفارشت ناقص است» (۴۰۰) یا «این غذا را نداریم» (۴۰۴).

کلاینت یک درخواست HTTP با متد و مسیر می‌فرستد؛ Flask تابع مربوط به آن مسیر را صدا می‌زند و مقدار برگشتی را به جواب JSON با کد وضعیت تبدیل می‌کند.
بخش نمونه معنی
متد GET، POST، PUT، DELETE چه کاری: خواندن، ساختن، جایگزینی، حذف
مسیر /servers/web1 روی چه چیزی
query string ?min_disk=80 پارامترهای اختیاری بعد از ?
بدنه (body) {"name": "web2", ...} داده‌ای که همراه POST فرستاده می‌شود
کد وضعیت 200، 201، 400، 404 نتیجه: موفق، ساخته شد، درخواست غلط، پیدا نشد

مثال ۱: آماده کردن پروژه

Section titled “مثال ۱: آماده کردن پروژه”

یک پوشه‌ی جدا با محیط مجازی (درس قبل) بساز و Flask را نصب کن:

Terminal window
mkdir -p ~/py/api
cd ~/py/api
python3 -m venv .venv
source .venv/bin/activate
pip install flask requests
خروجی (نمونه، بخش آخر)
Successfully installed blinker-1.9.0 certifi-2025.1.31 charset-normalizer-3.4.1 click-8.1.8 flask-3.1.0 idna-3.10 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.2 requests-2.32.3 urllib3-2.3.0 werkzeug-3.1.3

نسخه‌ها روی سیستم تو ممکن است جدیدتر باشند. اگر pip به اینترنت وصل نشد، نکته‌ی میرور PyPI در درس قبل را ببین.

فایل app.py:

app.py
from flask import Flask, jsonify, request
app = Flask(__name__)
app.json.ensure_ascii = False
app.json.sort_keys = False
servers = [
{"name": "web1", "ip": "10.0.0.1", "disk": 72},
{"name": "db1", "ip": "10.0.0.5", "disk": 91},
]
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/servers")
def list_servers():
min_disk = request.args.get("min_disk", default=0, type=int)
result = [s for s in servers if s["disk"] >= min_disk]
return jsonify(result)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=True)
  • app = Flask(__name__) برنامه‌ی وب را می‌سازد. دو خط بعد می‌گویند JSON خروجی فارسی را خوانا نگه دارد (مثل ensure_ascii=False درس فایل‌ها) و ترتیب کلیدها را عوض نکند.
  • @app.get("/health") یک دکوراتور است (مثل @dataclass درس کلاس): می‌گوید «وقتی درخواست GET به مسیر /health آمد، تابع زیرم را صدا بزن».
  • تابعی که دیکشنری برگرداند، Flask خودکار آن را JSON می‌کند. برای لیست از jsonify استفاده کردیم.
  • request.args.get("min_disk", default=0, type=int) پارامتر query string را می‌خواند، به عدد تبدیل می‌کند و اگر نبود صفر می‌گذارد.
  • servers فعلاً یک لیست در حافظه است (با هر بار اجرای دوباره، تغییرها می‌روند). در دوره‌ی SQL جایش را یک دیتابیس می‌گیرد.

اجرایش کن:

Terminal window
python3 app.py
خروجی
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 151-402-540

برنامه حالا روی پورت ۵۰۰۰ گوش می‌دهد و ترمینال را نگه داشته (تا Ctrl+C). آن هشدار مهم است و در «پشت پرده» به آن برمی‌گردیم.

یک ترمینال دوم باز کن (سرور را در اولی رها کن):

Terminal window
curl http://127.0.0.1:5000/health
curl "http://127.0.0.1:5000/servers?min_disk=80"
خروجی
{
"status": "ok"
}
[
{
"name": "db1",
"ip": "10.0.0.5",
"disk": 91
}
]

آدرس دارای ? را در کوتیشن بگذار تا shell آن را تفسیر نکند. با -i سرآیندهای (headers) جواب را هم می‌بینی:

Terminal window
curl -i http://127.0.0.1:5000/servers
خروجی
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.3
Date: Wed, 21 Oct 2026 16:29:07 GMT
Content-Type: application/json
Content-Length: 134
Connection: close
[
{
"name": "web1",
"ip": "10.0.0.1",
"disk": 72
},
{
"name": "db1",
"ip": "10.0.0.5",
"disk": 91
}
]

200 OK کد وضعیت است و Content-Type: application/json به کلاینت می‌گوید بدنه JSON است. در ترمینال اول هم برای هر درخواست یک خط لاگ می‌بینی، با همان شکل لاگ nginx:

ترمینال اول
127.0.0.1 - - [21/Oct/2026 19:59:07] "GET /servers HTTP/1.1" 200 -

مثال ۴: پارامتر در مسیر و ۴۰۴

Section titled “مثال ۴: پارامتر در مسیر و ۴۰۴”

این را به app.py (بالای if __name__) اضافه کن. Flask با debug=True هر بار که فایل را ذخیره کنی خودش سرور را دوباره بالا می‌آورد:

app.py (ادامه)
@app.get("/servers/<name>")
def get_server(name):
for s in servers:
if s["name"] == name:
return s
return {"error": f"سرور {name} پیدا نشد"}, 404
Terminal window
curl http://127.0.0.1:5000/servers/web1
curl -i http://127.0.0.1:5000/servers/nope
خروجی
{
"name": "web1",
"ip": "10.0.0.1",
"disk": 72
}
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.1.3 Python/3.12.3
Date: Wed, 21 Oct 2026 16:29:35 GMT
Content-Type: application/json
Content-Length: 47
Connection: close
{
"error": "سرور nope پیدا نشد"
}
  • <name> در مسیر یعنی «هر چه اینجا آمد را به‌عنوان پارامتر name به تابع بده».
  • return بدنه, 404 یعنی «این را برگردان، با کد وضعیت ۴۰۴». پیش‌فرض ۲۰۰ است.

کد وضعیت درست مهم است: برنامه‌ای که API تو را صدا می‌زند معمولاً اول کد را نگاه می‌کند، نه متن پیام را.

⚡ بررسی سریع

تابعی در Flask که return {'error': 'x'}, 400 می‌کند، به کلاینت چه می‌فرستد؟

مثال ۵: POST، ساختن با اعتبارسنجی

Section titled “مثال ۵: POST، ساختن با اعتبارسنجی”
app.py (ادامه)
@app.post("/servers")
def add_server():
data = request.get_json(silent=True)
if not data or "name" not in data or "ip" not in data:
return {"error": "name و ip لازم است"}, 400
if any(s["name"] == data["name"] for s in servers):
return {"error": "این اسم تکراری است"}, 409
server = {"name": data["name"], "ip": data["ip"], "disk": int(data.get("disk", 0))}
servers.append(server)
return server, 201
  • request.get_json(silent=True) بدنه‌ی JSON درخواست را به دیکشنری تبدیل می‌کند؛ silent=True یعنی اگر بدنه JSON نبود، به‌جای خطا None بده تا خودمان پیام درست برگردانیم.
  • هر چیزی که از بیرون می‌آید را بررسی کن: کلیدهای لازم هست؟ تکراری نیست؟ عدد واقعاً عدد است؟
  • ۴۰۰ (Bad Request) یعنی «درخواستت ناقص یا غلط است»، ۴۰۹ (Conflict) یعنی «با وضعیت فعلی تداخل دارد» و ۲۰۱ (Created) یعنی «ساخته شد».

با curl درخواست POST بفرست. -X POST متد، -H سرآیند و -d بدنه است؛ -w کد وضعیت را در آخر چاپ می‌کند:

Terminal window
curl -X POST http://127.0.0.1:5000/servers \
-H "Content-Type: application/json" \
-d '{"name": "cache1", "ip": "10.0.0.9", "disk": 15}' \
-w "%{http_code}\n"
curl -X POST http://127.0.0.1:5000/servers -H "Content-Type: application/json" -d '{"name": "x"}' -w "%{http_code}\n"
curl -X POST http://127.0.0.1:5000/servers -H "Content-Type: application/json" -d '{"name": "cache1", "ip": "1.1.1.1"}' -w "%{http_code}\n"
خروجی
{
"name": "cache1",
"ip": "10.0.0.9",
"disk": 15
}
201
{
"error": "name و ip لازم است"
}
400
{
"error": "این اسم تکراری است"
}
409

\ آخر خط اول یعنی «دستور در خط بعد ادامه دارد» (برای خوانایی دستورهای طولانی). حالا curl http://127.0.0.1:5000/servers سه سرور نشان می‌دهد.

مثال ۶: صدا زدن API از پایتون، requests

Section titled “مثال ۶: صدا زدن API از پایتون، requests”

API فقط برای curl نیست؛ هر برنامه‌ای می‌تواند صدایش بزند. در ترمینال دوم (با venv فعال):

client.py
import requests
BASE = "http://127.0.0.1:5000"
r = requests.get(f"{BASE}/servers", params={"min_disk": 80}, timeout=5)
print(r.status_code, r.headers["Content-Type"])
for s in r.json():
print(s["name"], s["disk"])
r = requests.post(f"{BASE}/servers", json={"name": "web2", "ip": "10.0.0.2", "disk": 40}, timeout=5)
print(r.status_code, r.json())
r = requests.get(f"{BASE}/servers/zzz", timeout=5)
print(r.status_code, r.json()["error"])
خروجی
200 application/json
db1 91
201 {'name': 'web2', 'ip': '10.0.0.2', 'disk': 40}
404 سرور zzz پیدا نشد
  • params= query string را می‌سازد (?min_disk=80)، json= دیکشنری را JSON می‌کند و Content-Type را هم خودش می‌گذارد.
  • r.status_code کد وضعیت، r.json() بدنه به شکل داده‌ی پایتون.
  • timeout=5 را هیچ‌وقت فراموش نکن: بدون آن، اگر سرور جواب ندهد، برنامه‌ات تا ابد منتظر می‌ماند.

مثال ۷: وقتی سرور خاموش است

Section titled “مثال ۷: وقتی سرور خاموش است”

سرور را در ترمینال اول با Ctrl+C متوقف کن و دوباره امتحان کن:

Terminal window
curl http://127.0.0.1:5000/health
خروجی
curl: (7) Failed to connect to 127.0.0.1 port 5000 after 0 ms: Couldn't connect to server

همان Connection refused درس پورت‌ها: کسی روی پورت ۵۰۰۰ گوش نمی‌دهد. در پایتون این requests.exceptions.ConnectionError است و باید مثل هر خطای دیگری (درس قبل) با try/except گرفته شود؛ تمرین متوسط.

پشت پرده: سرور توسعه و production

Section titled “پشت پرده: سرور توسعه و production”

python3 app.py سرور توسعه‌ی داخلی Flask (Werkzeug) را بالا می‌آورد: ساده، با بارگذاری خودکار و دیباگر. هشدار Do not use it in a production deployment جدی است:

سرور توسعه برای نوشتن و آزمایش روی کامپیوتر خودت است؛ روی سرور واقعی، یک سرور WSGI مثل gunicorn برنامه را اجرا می‌کند و Nginx جلویش می‌نشیند.
  • debug=True هرگز روی سرور در دسترس اینترنت. دیباگر Werkzeug در صفحه‌ی خطا یک کنسول پایتون می‌دهد (با PIN محافظت می‌شود، ولی نباید به آن تکیه کرد): کسی که به آن برسد می‌تواند کد دلخواه روی سرورت اجرا کند.
  • host="127.0.0.1" یعنی فقط از همین ماشین در دسترس است. host="0.0.0.0" یعنی روی همه‌ی کارت‌های شبکه گوش بده (درس پورت‌ها)؛ فقط وقتی لازم است و فقط پشت فایروال.
  • روی سرور واقعی، gunicorn (یا uvicorn برای فریم‌ورک‌های async مثل FastAPI) چند پروسه‌ی worker اجرا می‌کند، systemd آن را زنده نگه می‌دارد و Nginx با HTTPS جلویش می‌نشیند. همین زنجیره را در دوره‌ی Nginx (درس reverse proxy) و در پروژه‌ی پایانی همین دوره می‌بینی.
Flask کار
@app.get("/path") / @app.post("/path") تعریف مسیر برای یک متد
"/servers/<name>" پارامتر در مسیر
request.args.get("k", default=..., type=int) خواندن query string
request.get_json(silent=True) خواندن بدنه‌ی JSON
return {...} جواب JSON با ۲۰۰
return {...}, 404 جواب با کد دلخواه
jsonify(لیست) جواب JSON برای لیست
app.run(host=..., port=..., debug=...) سرور توسعه
کد معنی کی
200 OK خواندن موفق
201 Created ساختن موفق (POST)
400 Bad Request ورودی ناقص یا نامعتبر
404 Not Found منبع نیست
405 Method Not Allowed مسیر هست ولی این متد را ندارد
409 Conflict تداخل (مثلاً تکراری)
500 Internal Server Error باگ در کد سرور
curl کار
curl URL GET
curl -i URL با سرآیندها
curl -X POST -H "Content-Type: application/json" -d '{...}' URL POST با JSON
curl -w "%{http_code}\n" URL چاپ کد وضعیت

اگر سرور قبلی هنوز در یک ترمینال دیگر اجراست:

خروجی
Address already in use
Port 5000 is in use by another program. Either identify and stop that program, or start the server with a different port.

راه‌حل: سرور قبلی را با Ctrl+C ببند، یا با ss -tlnp | grep 5000 (درس پورت‌ها) پیدایش کن، یا پورت دیگری بده (port=5001).

curl -X DELETE http://127.0.0.1:5000/servers/web1 جواب 405 METHOD NOT ALLOWED می‌دهد، چون برای این مسیر فقط GET تعریف کرده‌ایم. راه‌حل: یا متد درست، یا مسیر جدید با @app.delete(...) (تمرین سخت).

۳) فراموش کردن Content-Type در POST

Section titled “۳) فراموش کردن Content-Type در POST”

بدون -H "Content-Type: application/json"، Flask بدنه را JSON نمی‌داند و get_json(silent=True) مقدار None می‌دهد؛ API ما ۴۰۰ برمی‌گرداند. راه‌حل: سرآیند را بگذار (یا از requests با json= استفاده کن که خودش می‌گذارد).

اگر int(data.get("disk", 0)) را بدون بررسی بنویسی و کسی "disk": "زیاد" بفرستد، ValueError رخ می‌دهد و کلاینت 500 می‌گیرد. راه‌حل: هر ورودی بیرونی را بررسی کن و با try/except به ۴۰۰ تبدیلش کن. هر چیزی از اینترنت می‌آید، ممکن است عمداً خراب باشد (دوره‌ی امنیت).

مثل «پشت پرده». راه‌حل: روی سرور واقعی debug خاموش و برنامه پشت gunicorn و Nginx.

تمرینآسان

یک مسیر GET /stats اضافه کن که تعداد سرورها و میانگین درصد دیسک را برگرداند، مثل {"count": 2, "avg_disk": 81.5}.

دیدن جواب
app.py (ادامه)
@app.get("/stats")
def stats():
disks = [s["disk"] for s in servers]
avg = round(sum(disks) / len(disks), 1) if disks else 0
return {"count": len(servers), "avg_disk": avg}
Terminal window
curl http://127.0.0.1:5000/stats
خروجی (بعد از اجرای تازه‌ی سرور)
{
"count": 2,
"avg_disk": 81.5
}

if disks else 0 جلوی تقسیم بر صفر را وقتی لیست خالی است می‌گیرد.

تمرینمتوسط

تمرین اصلی: یک اسکریپت monitor.py بنویس که با requests همه‌ی سرورها را از API بگیرد و برای هر سروری که دیسکش بیشتر از ۹۰٪ است یک هشدار چاپ کند. اگر API در دسترس نبود (ConnectionError) یا کد وضعیت ۲۰۰ نبود، پیام خطای خوانا بدهد و با کد خروج ۱ تمام شود.

دیدن جواب
monitor.py
import sys
import requests
try:
r = requests.get("http://127.0.0.1:5000/servers", timeout=5)
except requests.exceptions.ConnectionError:
print("خطا: API در دسترس نیست")
sys.exit(1)
if r.status_code != 200:
print(f"خطا: API کد {r.status_code} داد")
sys.exit(1)
for s in r.json():
if s["disk"] > 90:
print(f"هشدار: دیسک {s['name']} {s['disk']}٪ پر است")
خروجی (سرور روشن)
هشدار: دیسک db1 91٪ پر است
خروجی (سرور خاموش)
خطا: API در دسترس نیست

این اسکریپت را می‌شود با cron (دوره‌ی لینوکس) هر پنج دقیقه اجرا کرد؛ کد خروج ۱ هم به هر ابزار دیگری می‌گوید مشکلی هست.

تمرینسخت

مسیر DELETE /servers/<name> اضافه کن که سرور را حذف کند و 204 (بدون بدنه) برگرداند، یا اگر نبود 404. با curl -i -X DELETE امتحانش کن. (راهنما: در Flask، return "", 204.)

دیدن جواب
app.py (ادامه)
@app.delete("/servers/<name>")
def delete_server(name):
for i, s in enumerate(servers):
if s["name"] == name:
servers.pop(i)
return "", 204
return {"error": f"سرور {name} پیدا نشد"}, 404
Terminal window
curl -i -X DELETE http://127.0.0.1:5000/servers/web1
curl -s -X DELETE http://127.0.0.1:5000/servers/web1 -w "%{http_code}\n"
خروجی
HTTP/1.1 204 NO CONTENT
Server: Werkzeug/3.1.3 Python/3.12.3
Date: Wed, 21 Oct 2026 16:41:12 GMT
Content-Type: text/html; charset=utf-8
Connection: close
{
"error": "سرور web1 پیدا نشد"
}
404

بار اول حذف شد (۲۰۴، بدون بدنه)؛ بار دوم دیگر وجود نداشت (۴۰۴). enumerate اندیس را می‌دهد تا pop(i) همان عضو را بردارد.

آزمونک
  1. @app.get("/health") در Flask چه می‌کند؟

  2. برای ساختن موفق یک منبع با POST کدام کد مناسب است؟

  3. کلاینت فیلد اجباری را نفرستاده. کدام کد؟

  4. چرا debug=True روی سرور اینترنتی خطرناک است؟

  5. در requests.get(url, timeout=5)، timeout چه فایده‌ای دارد؟

  6. curl به یک مسیر GET-only با -X DELETE چه می‌گیرد؟

  • API = برنامه‌ای که درخواست HTTP می‌گیرد و (معمولاً) JSON برمی‌گرداند؛ متد + مسیر + کد وضعیت + بدنه.
  • Flask: @app.get / @app.post، پارامتر مسیر <name>، request.args و request.get_json، return داده, کد.
  • همیشه ورودی بیرونی را بررسی کن و کد درست بده: ۲۰۰، ۲۰۱، ۴۰۰، ۴۰۴، ۴۰۹.
  • آزمایش با curl (-i، -X، -H، -d، -w)؛ صدا زدن از پایتون با requests و همیشه با timeout.
  • python3 app.py فقط برای توسعه؛ production با gunicorn + systemd + Nginx، و هرگز debug=True.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
pip install flask requestsنصب در venv
@app.get("/servers/<name>")مسیر GET با پارامتر
request.args.get("k", default=0, type=int)query string
request.get_json(silent=True)بدنه‌ی JSON
return {"error": "..."}, 404جواب با کد
python3 app.pyسرور توسعه روی 127.0.0.1:5000
curl -i URLدیدن کد و سرآیندها
curl -X POST -H "Content-Type: application/json" -d '{...}' URLفرستادن JSON
requests.get(url, params={...}, timeout=5)صدا زدن از پایتون
gunicorn -w 4 app:appاجرای production (پشت Nginx)