🔝 الدليل الرئيسي للسلسلة: بايثون: لغة ومنظومة وأطر للمطوّرين
Flask هو أحد أكثر أطر الويب الصغيرة شعبية في عالم بايثون. فلسفته بسيطة: تقديم الآليات الأساسية لتطبيق ويب (توجيه URL، معالجة طلبات HTTP، عرض الاستجابات) دون فرض هيكل أو تبعيات إضافية. يبني هذا الدرس واجهة REST وظيفية من الصفر مع Flask 3.1: إنشاء المشروع، تعريف مسارات CRUD، التحقق من البيانات، معالجة الأخطاء والاختبار بـ curl. في نهاية هذا الدليل ستحصل على API عملية تخدم بيانات JSON مُهيكَلة وفق أفضل الممارسات.
المتطلبات
- Python 3.12 أو 3.13 مُثبَّت (راجع درس تثبيت Python وإعداد البيئة)
- معرفة دنيا ببايثون (متغيّرات، دوال، قوائم، قواميس)
- طرفية ومحرّر كود (VS Code مُوصى به)
- curl مُثبَّت لاختبار النقاط الطرفية (مُضمَّن افتراضيًا على Linux وmacOS وWindows 10+)
- المستوى: مبتدئ إلى متوسط
- الوقت المُقدَّر: 45 إلى 60 دقيقة
الخطوة 1 — إنشاء المشروع وتثبيت Flask
كل مشروع Flask يجب أن يعيش في بيئته الافتراضية الخاصة لعزل تبعياته. ابدأ بإنشاء مجلد المشروع وتهيئة البيئة الافتراضية:
mkdir api-flask-demo
cd api-flask-demo
python3.13 -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\Activate.ps1 # Windows PowerShell
which python # Doit pointer vers .venv/bin/python
بعد تفعيل البيئة، يعرض موجه الطرفية (.venv) كبادئة. which python (أو where python على Windows) يؤكد استخدام مفسّر venv.
pip install flask
python -c "import flask; print(flask.__version__)"
pip freeze > requirements.txt
pip install flask يُثبّت Flask وتبعياته: Werkzeug (WSGI، routing، أدوات HTTP)، Jinja2 (محرك القوالب)، Click (واجهة سطر الأوامر)، itsdangerous (تأمين الكوكيز والـ tokens) وMarkupSafe (تهريب HTML). يجب أن يعرض أمر التحقق 3.1.0 أو أحدث.
الخطوة 2 — إنشاء هيكل المشروع
Flask لا يفرض هيكل مشروع معيّن، لكن تنظيم واضح يُسهّل الصيانة. لـ API REST بسيطة:
api-flask-demo/
├── .venv/ # Environnement virtuel (ignoré par git)
├── app.py # Point d'entrée de l'application
├── requirements.txt # Dépendances épinglées
└── .gitignore
للمشاريع الأكبر، Flask يدعم Blueprints لتنظيم الكود في وحدات. لهذا الدرس التمهيدي، ملف واحد app.py يكفي:
# app.py
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# Base de données en mémoire (dictionnaire pour ce tutoriel)
produits = {
1: {"id": 1, "nom": "Laptop ThinkPad", "prix": 850000, "stock": 10},
2: {"id": 2, "nom": "Souris USB", "prix": 15000, "stock": 50},
3: {"id": 3, "nom": "Clé USB 64GB", "prix": 8000, "stock": 200},
}
prochain_id = 4
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)
الكائن app = Flask(__name__) هو المثيل المركزي للتطبيق. القاموس produits يلعب دور قاعدة بيانات في الذاكرة — البيانات تُفقد عند إعادة التشغيل. كتلة if __name__ == "__main__" تُشغّل خادم Flask للتطوير فقط عند تنفيذ الملف مباشرة.
الخطوة 3 — مسار GET: قائمة كل المنتجات
أول مسار للتنفيذ هو قائمة المنتجات. في REST، طلب GET على المجموعة (/api/produits) يُرجع قائمة جميع الموارد.
@app.route("/api/produits", methods=["GET"])
def lister_produits():
"""Retourne la liste complète des produits au format JSON."""
liste = list(produits.values())
return jsonify({
"succes": True,
"total": len(liste),
"produits": liste
}), 200
المُزخرف @app.route يُسجّل الدالة كمعالج لطلبات GET. الدالة jsonify() تُحوّل قاموس بايثون إلى استجابة HTTP بـ Content-Type application/json. اختبر:
python app.py
# Dans un second terminal
curl -s http://localhost:5000/api/produits | python3 -m json.tool
وضع debug يُفعّل إعادة التحميل التلقائي عند تعديل الكود — يجب تعطيله إلزاميًا في الإنتاج.
الخطوة 4 — مسار GET بـ ID: استرجاع منتج
Flask يلتقط أجزاء متغيّرة في URL عبر صياغة <type:name>:
@app.route("/api/produits/<int:produit_id>", methods=["GET"])
def obtenir_produit(produit_id):
"""Retourne un produit spécifique par son ID, ou 404 si introuvable."""
produit = produits.get(produit_id)
if produit is None:
abort(404)
return jsonify({"succes": True, "produit": produit}), 200
المعامل <int:produit_id> يُشير لـ Flask أن هذا الجزء يجب تحويله لعدد صحيح. إذا احتوى URL على قيمة غير قابلة للتحويل (مثل /api/produits/abc)، يُرجع Flask 404 تلقائيًا دون استدعاء الدالة. produits.get(produit_id) يُرجع None إذا لم يوجد المفتاح — آمن أكثر من produits[produit_id].
curl -s http://localhost:5000/api/produits/1 | python3 -m json.tool
curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/api/produits/999
الخطوة 5 — مسار POST: إضافة منتج
@app.route("/api/produits", methods=["POST"])
def creer_produit():
"""Crée un nouveau produit à partir des données JSON reçues."""
global prochain_id
donnees = request.get_json(silent=True)
if donnees is None:
return jsonify({"succes": False, "erreur": "Corps JSON invalide ou absent"}), 400
champs_requis = ["nom", "prix", "stock"]
champs_manquants = [c for c in champs_requis if c not in donnees]
if champs_manquants:
return jsonify({
"succes": False,
"erreur": f"Champs manquants : {', '.join(champs_manquants)}"
}), 422
if not isinstance(donnees["prix"], (int, float)) or donnees["prix"] <= 0:
return jsonify({"succes": False, "erreur": "Le prix doit être un nombre positif"}), 422
nouveau_produit = {
"id": prochain_id,
"nom": str(donnees["nom"]).strip(),
"prix": float(donnees["prix"]),
"stock": int(donnees.get("stock", 0))
}
produits[prochain_id] = nouveau_produit
prochain_id += 1
return jsonify({"succes": True, "produit": nouveau_produit}), 201
request.get_json(silent=True) يُحلّل جسم الطلب JSON ويُرجع None دون رفع استثناء عند فشل التحليل. silent=True ضروري للتحكم النظيف في الأخطاء. كود HTTP 201 Created (وليس 200) يُشير إلى إنشاء مورد — اصطلاح REST قياسي.
curl -s -X POST http://localhost:5000/api/produits \
-H "Content-Type: application/json" \
-d '{"nom": "Clavier mécanique", "prix": 45000, "stock": 25}' \
| python3 -m json.tool
# Test validation : champ manquant
curl -s -X POST http://localhost:5000/api/produits \
-H "Content-Type: application/json" \
-d '{"nom": "Test"}' | python3 -m json.tool
الخطوة 6 — مسار PUT: تعديل منتج
@app.route("/api/produits/<int:produit_id>", methods=["PUT"])
def modifier_produit(produit_id):
"""Met à jour les champs fournis d'un produit existant."""
produit = produits.get(produit_id)
if produit is None:
abort(404)
donnees = request.get_json(silent=True)
if donnees is None:
return jsonify({"succes": False, "erreur": "Corps JSON invalide"}), 400
if "nom" in donnees:
produit["nom"] = str(donnees["nom"]).strip()
if "prix" in donnees:
if not isinstance(donnees["prix"], (int, float)) or donnees["prix"] <= 0:
return jsonify({"succes": False, "erreur": "Prix invalide"}), 422
produit["prix"] = float(donnees["prix"])
if "stock" in donnees:
produit["stock"] = int(donnees["stock"])
return jsonify({"succes": True, "produit": produit}), 200
التحديث الانتقائي بـ if "nom" in donnees يتيح للعميل إرسال الحقول المُراد تعديلها فقط دون مسح الباقي. القواميس في بايثون تُمرَّر بالمرجع، فتعديل produit مباشرة يُعدّل القيمة في القاموس produits.
curl -s -X PUT http://localhost:5000/api/produits/1 \
-H "Content-Type: application/json" \
-d '{"prix": 920000}' | python3 -m json.tool
الخطوة 7 — مسار DELETE: حذف منتج
@app.route("/api/produits/<int:produit_id>", methods=["DELETE"])
def supprimer_produit(produit_id):
"""Supprime un produit par son ID."""
produit = produits.pop(produit_id, None)
if produit is None:
abort(404)
return jsonify({
"succes": True,
"message": f"Produit #{produit_id} supprimé",
"produit_supprime": produit
}), 200
dict.pop(key, default) يحذف ويُرجع القيمة، أو default إذا لم يوجد المفتاح — يتجنّب وصولين مزدوجين للقاموس.
الخطوة 8 — معالجة الأخطاء عالميًا
Flask يُتيح تسجيل معالجات أخطاء عالمية. بدونها، أخطاء 404 و500 تُرجع HTML افتراضيًا — غير متوافق مع API JSON.
@app.errorhandler(404)
def non_trouve(erreur):
return jsonify({"succes": False, "erreur": "Ressource introuvable"}), 404
@app.errorhandler(405)
def methode_non_autorisee(erreur):
return jsonify({"succes": False, "erreur": "Méthode HTTP non autorisée"}), 405
@app.errorhandler(500)
def erreur_interne(erreur):
return jsonify({"succes": False, "erreur": "Erreur interne du serveur"}), 500
هذه المعالجات تضمن أن الأخطاء غير المتوقعة تُرجع JSON بدلًا من HTML.
curl -s http://localhost:5000/api/inexistant | python3 -m json.tool
الخطوة 9 — التحقق النهائي
BASE="http://localhost:5000/api/produits"
echo "=== GET tous les produits ==="
curl -s $BASE | python3 -m json.tool
echo "=== GET produit #2 ==="
curl -s $BASE/2 | python3 -m json.tool
echo "=== POST nouveau produit ==="
curl -s -X POST $BASE \
-H "Content-Type: application/json" \
-d '{"nom":"Moniteur 24 pouces","prix":180000,"stock":8}' \
| python3 -m json.tool
echo "=== PUT modifier prix produit #1 ==="
curl -s -X PUT $BASE/1 \
-H "Content-Type: application/json" \
-d '{"prix":880000}' | python3 -m json.tool
echo "=== DELETE produit #3 ==="
curl -s -X DELETE $BASE/3 | python3 -m json.tool
echo "=== GET vérification après DELETE ==="
curl -s $BASE | python3 -m json.tool
هذا السكربت يُسلسل 5 عمليات CRUD: قائمة أولية، قراءة عنصر، إنشاء، تحديث، حذف، ثم قائمة نهائية. الناتج النهائي يجب أن يعرض 3 منتجات (رقم 3 محذوف، رقم 4 جديد) مع المنتج رقم 1 بالسعر الجديد.
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
ImportError: No module named 'flask' |
Flask مُثبَّت خارج venv | تفعيل venv ثم إعادة pip install flask |
| POST يُرجع « Corps JSON invalide » | Header Content-Type مفقود في curl | إضافة -H "Content-Type: application/json" |
| Port 5000 مستخدم (macOS) | AirPlay Receiver يحتل المنفذ 5000 على Monterey+ | تغيير المنفذ: app.run(port=5001) |
| التعديلات لا تُحمَّل تلقائيًا | وضع debug غير مُفعَّل | التحقق من debug=True في app.run() |
| البيانات تُفقد بعد إعادة التشغيل | تخزين في الذاكرة (قاموس) | طبيعي لهذا الدرس — استخدم SQLAlchemy + SQLite للحفظ |
الأسئلة الشائعة
ما الفرق بين Flask وFastAPI لـ API REST؟
Flask متزامن افتراضيًا (يدعم async منذ v2.0 لكن اختياريًا) ولا يقوم بالتحقق التلقائي من البيانات. FastAPI غير متزامن أصلًا، يتحقق من البيانات عبر Pydantic ويُولّد توثيق Swagger/OpenAPI تلقائيًا. لـ API بسيطة سريعة الإطلاق، Flask ممتاز. لـ API إنتاج بتنميط صارم وتوثيق تلقائي، FastAPI الخيار الأفضل في 2024.
كيف ننقل Flask إلى الإنتاج؟
خادم تطوير Flask (app.run(debug=True)) لا يجب استخدامه في الإنتاج أبدًا. في الإنتاج، يُخدم Flask عبر خادم WSGI قوي مثل Gunicorn (gunicorn app:app) أو uWSGI، خلف proxy عكسي Nginx أو Caddy يُدير SSL/TLS، الضغط، والملفات الثابتة.
كيف نُضيف قاعدة بيانات حقيقية لهذه الـ API؟
الخطوة الطبيعية التالية هي دمج Flask-SQLAlchemy مع SQLite (للتطوير) أو PostgreSQL (للإنتاج). Flask-SQLAlchemy يقدّم ORM يُربط جداول قاعدة البيانات بأصناف بايثون. عمليات CRUD تصبح استدعاءات لـ db.session.add()، db.session.commit()، Model.query.get(id) — بديلًا عن القاموس في الذاكرة.