یک 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 است.
تشبیه: پیشخوان رستوران
Section titled “تشبیه: پیشخوان رستوران”API مثل پیشخوان سفارش یک رستوران است. مشتری (کلاینت) لازم نیست بداند آشپزخانه چطور کار میکند؛ فقط از منو (مسیرهای API) انتخاب میکند: «یک فهرست سرور بده» (GET /servers) یا «این سرور جدید را ثبت کن» (POST /servers). پیشخوان جواب میدهد: «بفرمایید» (۲۰۰)، «ثبت شد» (۲۰۱)، «سفارشت ناقص است» (۴۰۰) یا «این غذا را نداریم» (۴۰۴).
یادآوری سریع HTTP
Section titled “یادآوری سریع HTTP”| بخش | نمونه | معنی |
|---|---|---|
| متد | GET، POST، PUT، DELETE |
چه کاری: خواندن، ساختن، جایگزینی، حذف |
| مسیر | /servers/web1 |
روی چه چیزی |
| query string | ?min_disk=80 |
پارامترهای اختیاری بعد از ? |
| بدنه (body) | {"name": "web2", ...} |
دادهای که همراه POST فرستاده میشود |
| کد وضعیت | 200، 201، 400، 404 |
نتیجه: موفق، ساخته شد، درخواست غلط، پیدا نشد |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: آماده کردن پروژه
Section titled “مثال ۱: آماده کردن پروژه”یک پوشهی جدا با محیط مجازی (درس قبل) بساز و Flask را نصب کن:
mkdir -p ~/py/apicd ~/py/apipython3 -m venv .venvsource .venv/bin/activatepip install flask requestsSuccessfully 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 در درس قبل را ببین.
مثال ۲: اولین API
Section titled “مثال ۲: اولین API”فایل app.py:
from flask import Flask, jsonify, request
app = Flask(__name__)app.json.ensure_ascii = Falseapp.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 جایش را یک دیتابیس میگیرد.
اجرایش کن:
python3 app.py * Serving Flask app 'app' * Debug mode: onWARNING: 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:5000Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 151-402-540برنامه حالا روی پورت ۵۰۰۰ گوش میدهد و ترمینال را نگه داشته (تا Ctrl+C). آن هشدار مهم است و در «پشت پرده» به آن برمیگردیم.
مثال ۳: آزمایش با curl
Section titled “مثال ۳: آزمایش با curl”یک ترمینال دوم باز کن (سرور را در اولی رها کن):
curl http://127.0.0.1:5000/healthcurl "http://127.0.0.1:5000/servers?min_disk=80"{ "status": "ok"}[ { "name": "db1", "ip": "10.0.0.5", "disk": 91 }]آدرس دارای ? را در کوتیشن بگذار تا shell آن را تفسیر نکند. با -i سرآیندهای (headers) جواب را هم میبینی:
curl -i http://127.0.0.1:5000/serversHTTP/1.1 200 OKServer: Werkzeug/3.1.3 Python/3.12.3Date: Wed, 21 Oct 2026 16:29:07 GMTContent-Type: application/jsonContent-Length: 134Connection: 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.get("/servers/<name>")def get_server(name): for s in servers: if s["name"] == name: return s return {"error": f"سرور {name} پیدا نشد"}, 404curl http://127.0.0.1:5000/servers/web1curl -i http://127.0.0.1:5000/servers/nope{ "name": "web1", "ip": "10.0.0.1", "disk": 72}HTTP/1.1 404 NOT FOUNDServer: Werkzeug/3.1.3 Python/3.12.3Date: Wed, 21 Oct 2026 16:29:35 GMTContent-Type: application/jsonContent-Length: 47Connection: close
{ "error": "سرور nope پیدا نشد"}<name>در مسیر یعنی «هر چه اینجا آمد را بهعنوان پارامترnameبه تابع بده».return بدنه, 404یعنی «این را برگردان، با کد وضعیت ۴۰۴». پیشفرض ۲۰۰ است.
کد وضعیت درست مهم است: برنامهای که API تو را صدا میزند معمولاً اول کد را نگاه میکند، نه متن پیام را.
تابعی در Flask که return {'error': 'x'}, 400 میکند، به کلاینت چه میفرستد؟
جواب درست: کد 400 با بدنهی JSON شامل error
عضو دوم tuple کد وضعیت است؛ دیکشنری به JSON تبدیل میشود.
مثال ۵: POST، ساختن با اعتبارسنجی
Section titled “مثال ۵: POST، ساختن با اعتبارسنجی”@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, 201request.get_json(silent=True)بدنهی JSON درخواست را به دیکشنری تبدیل میکند؛silent=Trueیعنی اگر بدنه JSON نبود، بهجای خطاNoneبده تا خودمان پیام درست برگردانیم.- هر چیزی که از بیرون میآید را بررسی کن: کلیدهای لازم هست؟ تکراری نیست؟ عدد واقعاً عدد است؟
- ۴۰۰ (Bad Request) یعنی «درخواستت ناقص یا غلط است»، ۴۰۹ (Conflict) یعنی «با وضعیت فعلی تداخل دارد» و ۲۰۱ (Created) یعنی «ساخته شد».
با curl درخواست POST بفرست. -X POST متد، -H سرآیند و -d بدنه است؛ -w کد وضعیت را در آخر چاپ میکند:
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 فعال):
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/jsondb1 91201 {'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 متوقف کن و دوباره امتحان کن:
curl http://127.0.0.1:5000/healthcurl: (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 جدی است:
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) و در پروژهی پایانی همین دوره میبینی.
جدولهای مرجع
Section titled “جدولهای مرجع”| 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 |
چاپ کد وضعیت |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) پورت اشغال است
Section titled “۱) پورت اشغال است”اگر سرور قبلی هنوز در یک ترمینال دیگر اجراست:
Address already in usePort 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).
۲) متد اشتباه
Section titled “۲) متد اشتباه”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= استفاده کن که خودش میگذارد).
۴) اعتماد به ورودی
Section titled “۴) اعتماد به ورودی”اگر int(data.get("disk", 0)) را بدون بررسی بنویسی و کسی "disk": "زیاد" بفرستد، ValueError رخ میدهد و کلاینت 500 میگیرد. راهحل: هر ورودی بیرونی را بررسی کن و با try/except به ۴۰۰ تبدیلش کن. هر چیزی از اینترنت میآید، ممکن است عمداً خراب باشد (دورهی امنیت).
۵) debug=True روی سرور
Section titled “۵) debug=True روی سرور”مثل «پشت پرده». راهحل: روی سرور واقعی debug خاموش و برنامه پشت gunicorn و Nginx.
یک مسیر GET /stats اضافه کن که تعداد سرورها و میانگین درصد دیسک را برگرداند، مثل {"count": 2, "avg_disk": 81.5}.
دیدن جواب
@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}curl http://127.0.0.1:5000/stats{ "count": 2, "avg_disk": 81.5}if disks else 0 جلوی تقسیم بر صفر را وقتی لیست خالی است میگیرد.
تمرین اصلی: یک اسکریپت monitor.py بنویس که با requests همهی سرورها را از API بگیرد و برای هر سروری که دیسکش بیشتر از ۹۰٪ است یک هشدار چاپ کند. اگر API در دسترس نبود (ConnectionError) یا کد وضعیت ۲۰۰ نبود، پیام خطای خوانا بدهد و با کد خروج ۱ تمام شود.
دیدن جواب
import sysimport 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.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} پیدا نشد"}, 404curl -i -X DELETE http://127.0.0.1:5000/servers/web1curl -s -X DELETE http://127.0.0.1:5000/servers/web1 -w "%{http_code}\n"HTTP/1.1 204 NO CONTENTServer: Werkzeug/3.1.3 Python/3.12.3Date: Wed, 21 Oct 2026 16:41:12 GMTContent-Type: text/html; charset=utf-8Connection: close
{ "error": "سرور web1 پیدا نشد"}404بار اول حذف شد (۲۰۴، بدون بدنه)؛ بار دوم دیگر وجود نداشت (۴۰۴). enumerate اندیس را میدهد تا pop(i) همان عضو را بردارد.
آزمونک
Section titled “آزمونک”@app.get("/health") در Flask چه میکند؟
جواب درست: تابع زیرش را برای درخواستهای GET به مسیر /health ثبت میکند
یک دکوراتور مسیر است.
برای ساختن موفق یک منبع با POST کدام کد مناسب است؟
جواب درست: 201
201 Created.
کلاینت فیلد اجباری را نفرستاده. کدام کد؟
جواب درست: 400
400 Bad Request؛ 500 یعنی باگ سمت سرور.
چرا debug=True روی سرور اینترنتی خطرناک است؟
جواب درست: دیباگر امکان اجرای کد دلخواه روی سرور را میدهد
production: debug خاموش، پشت gunicorn و Nginx.
در requests.get(url, timeout=5)، timeout چه فایدهای دارد؟
جواب درست: اگر سرور جواب ندهد، برنامه بعد از ۵ ثانیه خطا میدهد بهجای انتظار بیپایان
بدون timeout برنامه ممکن است برای همیشه گیر کند.
curl به یک مسیر GET-only با -X DELETE چه میگیرد؟
جواب درست: 405 Method Not Allowed
مسیر هست ولی این متد تعریف نشده.
جمعبندی
Section titled “جمعبندی”- 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) |