تطوير REST API

تطوير REST API

تشبيه من الواقع

تخترق أنك تدير مطعمًا. يأتي العملاء (العملاء) إلى المكتب الأمامي، ويوجههم النادل (الموجه) إلى نوافذ مختلفة بناءً على احتياجاتهم:

يعالج المطبخ (الخادم) الطلب ويسلّم الطبق المُعد (الاستجابة) عبر النادل إلى العميل. في هذه الأثناء، يتحقق حارس الأمن (الوسائط) من الهوية قبل دخول العملاء، ويسجل أوقات الزيارة، ويمكنه التعامل مع الطوارئ.

واجهة برمجة REST هي سير عمل معياري مثل هذا — يستخدم العملاء "أفعالًا" موحدة (أساليب HTTP) و"عناوين" (عناوين URL) للتواصل مع الخادم، ويرجع الخادم بيانات منظمة (JSON).

متطلبات المشروع

سنطور واجهة برمجة REST للمهام تدعم الميزات التالية:

العملية الأسلوب المسار الوصف
الحصول على جميع المهام GET /api/todos يُرجع قائمة المهام
الحصول على مهمة واحدة GET /api/todos/{id} يُرجع التفاصيل حسب المعرّف
إنشاء مهمة POST /api/todos يضيف مهمة جديدة
تحديث مهمة PUT /api/todos/{id} يُعدّل المهمة المحددة
حذف مهمة DELETE /api/todos/{id} يحذف المهمة المحددة

متطلبات إضافية:

تصميم النظام

طلب العميل
    │
    ▼
┌──────────────────────────────┐
│         سلسلة الوسائط        │
│  الاستعادة → المصادقة → التسجيل │
│         ↓         ↓         ↓ │
└──────────────────────────────┘
    │
    ▼
┌──────────────────────────────┐
│          الموجه              │
│  GET    /api/todos           │
│  GET    /api/todos/{id}      │
│  POST   /api/todos           │
│  PUT    /api/todos/{id}      │
│  DELETE /api/todos/{id}      │
└──────────────────────────────┘
    │
    ▼
┌──────────────────────────────┐
│     طبقة التخزين في الذاكرة   │
│   map[string]Todo + sync.RWMutex │
└──────────────────────────────┘

الكود الكامل

GO
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
)

// هيكل عنصر Todo
type Todo struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

// TodoStore تخزين في الذاكرة مع قفل قراءة-كتابة لسلامة التزامن
type TodoStore struct {
	mu    sync.RWMutex
	todos map[string]Todo
}

// NewTodoStore ينشئ مثيل تخزين جديد
func NewTodoStore() *TodoStore {
	return &TodoStore{
		todos: make(map[string]Todo),
	}
}

// GetAll يُرجع جميع عناصر المهام
func (s *TodoStore) GetAll() []Todo {
	s.mu.RLock()
	defer s.mu.RUnlock()

	list := make([]Todo, 0, len(s.todos))
	for _, t := range s.todos {
		list = append(list, t)
	}
	return list
}

// GetByID يُرجع عنصر مهمة واحد حسب المعرّف
func (s *TodoStore) GetByID(id string) (Todo, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	t, ok := s.todos[id]
	return t, ok
}

// Create ينشئ عنصر مهمة جديد
func (s *TodoStore) Create(title string) Todo {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now().Format(time.RFC3339)
	t := Todo{
		ID:        uuid.New().String(),
		Title:     title,
		Completed: false,
		CreatedAt: now,
		UpdatedAt: now,
	}
	s.todos[t.ID] = t
	return t
}

// Update يُحدّث عنصر المهمة المحدد
func (s *TodoStore) Update(id string, title string, completed bool) (Todo, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	t, ok := s.todos[id]
	if !ok {
		return Todo{}, false
	}

	t.Title = title
	t.Completed = completed
	t.UpdatedAt = time.Now().Format(time.RFC3339)
	s.todos[id] = t
	return t, true
}

// Delete يحذف عنصر المهمة المحدد
func (s *TodoStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.todos[id]; !ok {
		return false
	}
	delete(s.todos, id)
	return true
}

// --- هياكل الاستجابة الشائعة ---

// APIResponse تنسيق استجابة API موحد
type APIResponse struct {
	Success bool        `json:"success"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
}

// jsonResponse يُرسل استجابة JSON
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

// --- الوسائط ---

// Middleware تعريف نوع الوسائط
type Middleware func(http.Handler) http.Handler

// RecoveryMiddleware وسائط استعادة الذعر
// يلتقط عمليات الذعر في دوال المعالجة، يُرجع خطأ 500 بدلاً من تعطل العملية
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("[PANIC] %v", err)
				jsonResponse(w, http.StatusInternalServerError, APIResponse{
					Success: false,
					Error:   "خطأ داخلي في الخادم",
				})
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// LoggingMiddleware وسائط تسجيل الطلبات
// يسجل أسلوب كل طلب والمسار والمدة
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("[LOG] %s %s %s", r.Method, r.URL.Path, time.Since(start))
	})
}

// AuthMiddleware وسائط مصادقة بسيطة
// يتحقق مما إذا كان رأس الطلب يحتوي على مفتاح API صالح
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		apiKey := r.Header.Get("X-API-Key")
		if apiKey != "secret-key-123" {
			jsonResponse(w, http.StatusUnauthorized, APIResponse{
				Success: false,
				Error:   "مفتاح API غير صالح",
			})
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Chain تجمع عدة وسائط في سلسلة
// ترتيب المعاملات هو ترتيب التنفيذ: Chain(A, B, C) → A(B(C(handler)))
func Chain(middlewares ...Middleware) Middleware {
	return func(next http.Handler) http.Handler {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
}

// --- الموجه والمعالجات ---

// Router موجه مخصص
type Router struct {
	routes     map[string]map[string]http.HandlerFunc // method -> path -> handler
	middleware Middleware
}

// NewRouter ينشئ موجهًا
func NewRouter() *Router {
	return &Router{
		routes: make(map[string]map[string]http.HandlerFunc),
	}
}

// Use يسجل وسائط عالمية
func (rt *Router) Use(m Middleware) {
	rt.middleware = m
}

// Handle يسجل مسارًا: أسلوب + مسار + معالج
func (rt *Router) Handle(method, path string, handler http.HandlerFunc) {
	if _, ok := rt.routes[method]; !ok {
		rt.routes[method] = make(map[string]http.HandlerFunc)
	}
	rt.routes[method][path] = handler
}

// ServeHTTP ينفذ واجهة http.Handler، مكملًا مطابقة المسار
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	methodRoutes, ok := rt.routes[r.Method]
	if !ok {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{
			Success: false,
			Error:   "أسلوب غير مسموح",
		})
		return
	}

	// مطابقة تامة
	if handler, ok := methodRoutes[r.URL.Path]; ok {
		handler(w, r)
		return
	}

	// مطابقة بادئة: محاولة استخراج معاملات المسار (مثل /api/todos/{id})
	for pattern, handler := range methodRoutes {
		if strings.HasSuffix(pattern, "/{id}") {
			prefix := strings.TrimSuffix(pattern, "/{id}")
			if strings.HasPrefix(r.URL.Path, prefix+"/") {
				// وضع {id} في رأس الطلب لقراءته من قبل المعالج
				id := strings.TrimPrefix(r.URL.Path, prefix+"/")
				r.Header.Set("X-Resource-ID", id)
				handler(w, r)
				return
			}
		}
	}

	jsonResponse(w, http.StatusNotFound, APIResponse{
		Success: false,
		Error:   "المورد غير موجود",
	})
}

// --- دوال المعالجة ---

// handleGetTodos يُرجع جميع عناصر المهام
func handleGetTodos(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		todos := store.GetAll()
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todos,
		})
	}
}

// handleGetTodo يُرجع عنصر مهمة واحد
func handleGetTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")
		todo, ok := store.GetByID(id)
		if !ok {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "عنصر المهمة غير موجود",
			})
			return
		}
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// CreateTodoRequest جسم الطلب لإنشاء مهمة
type CreateTodoRequest struct {
	Title string `json:"title"`
}

// handleCreateTodo ينشئ عنصر مهمة
func handleCreateTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req CreateTodoRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "جسم الطلب غير صالح",
			})
			return
		}

		if strings.TrimSpace(req.Title) == "" {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "العنوان لا يمكن أن يكون فارغًا",
			})
			return
		}

		todo := store.Create(req.Title)
		jsonResponse(w, http.StatusCreated, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// UpdateTodoRequest جسم الطلب لتحديث مهمة
type UpdateTodoRequest struct {
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

// handleUpdateTodo يُحدّث عنصر مهمة
func handleUpdateTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")

		var req UpdateTodoRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "جسم الطلب غير صالح",
			})
			return
		}

		if strings.TrimSpace(req.Title) == "" {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "العنوان لا يمكن أن يكون فارغًا",
			})
			return
		}

		todo, ok := store.Update(id, req.Title, req.Completed)
		if !ok {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "عنصر المهمة غير موجود",
			})
			return
		}

		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// handleDeleteTodo يحذف عنصر مهمة
func handleDeleteTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")
		if !store.Delete(id) {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "عنصر المهمة غير موجود",
			})
			return
		}
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    "تم الحذف بنجاح",
		})
	}
}

func main() {
	// تهيئة التخزين
	store := NewTodoStore()

	// إنشاء الموجه
	router := NewRouter()

	// تسجيل سلسلة الوسائط: الاستعادة → المصادقة → التسجيل
	router.Use(Chain(RecoveryMiddleware, AuthMiddleware, LoggingMiddleware))

	// تسجيل المسارات
	router.Handle("GET", "/api/todos", handleGetTodos(store))
	router.Handle("GET", "/api/todos/{id}", handleGetTodo(store))
	router.Handle("POST", "/api/todos", handleCreateTodo(store))
	router.Handle("PUT", "/api/todos/{id}", handleUpdateTodo(store))
	router.Handle("DELETE", "/api/todos/{id}", handleDeleteTodo(store))

	// تطبيق الوسائط وبدء تشغيل الخادم
	addr := ":8080"
	log.Printf("خادم REST API بدأ التشغيل على http://localhost%s", addr)
	log.Fatal(http.ListenAndServe(addr, router.middleware(router)))
}

التشغيل والاختبار

قبل بدء الخدمة، قم بتهيئة الوحدة وتثبيت التبعيات:

BASH
go mod init todo-api
go get github.com/google/uuid
go run main.go

استخدم curl لاختبار كل نقطة نهاية:

BASH
# إنشاء عنصر مهمة
curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret-key-123" \
  -d '{"title": "تعلم Go"}'

# الحصول على جميع المهام
curl http://localhost:8080/api/todos \
  -H "X-API-Key: secret-key-123"

# الحصول على مهمة واحدة (استبدل {id} بالمعرّف الفعلي المُرجع)
curl http://localhost:8080/api/todos/{id} \
  -H "X-API-Key: secret-key-123"

# تحديث مهمة
curl -X PUT http://localhost:8080/api/todos/{id} \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret-key-123" \
  -d '{"title": "تعلم Go (مكتمل)", "completed": true}'

# حذف مهمة
curl -X DELETE http://localhost:8080/api/todos/{id} \
  -H "X-API-Key: secret-key-123"

# اختبار طلب غير مصادق (يجب أن يُرجع 401)
curl http://localhost:8080/api/todos

أمثلة الاستجابات المتوقعة:

TEXT
# إنشاء ناجح
{"success":true,"data":{"id":"a1b2c3d4-...","title":"تعلم Go","completed":false,"created_at":"2026-06-27T10:00:00+08:00","updated_at":"2026-06-27T10:00:00+08:00"}}

# غير مصادق
{"success":false,"error":"مفتاح API غير صالح"}

تحليل الكود

1. نموذج البيانات وطبقة التخزين

يحدد هيكل Todo بنية البيانات لعناصر المهام، مستخدمًا علامات JSON للتحكم في أسماء الحقول المسلسلة. يستخدم TodoStore من sync.RWMutex لحماية map — تستخدم عمليات القراءة قفل قراءة (RLock)، وعمليات الكتابة قفل كتابة (Lock)، لضمان سلامة التزامن.

2. نمط الوسائط

كل وسائط من نوع func(http.Handler) http.Handler، يلتف المعالج التالي من خلال نمط الزخرفة:

TEXT
RecoveryMiddleware(
  AuthMiddleware(
    LoggingMiddleware(
      المعالج النهائي,
    ),
  ),
)

تجمع دالة Chain عدة وسائط بترتيب المعاملات في سلسلة.

3. الموجه المخصص

بما أن http.ServeMux في المكتبة القياسية لم يدعم معاملات المسار قبل Go 1.22، قمنا بتنفيذ موجه بسيط:

4. إغلاقات المعالج

دوال مثل handleGetTodos(store) تُرجع إغلاقًا يلتقط مرجع store. يسمح هذا للمعالجات ب الوصول إلى طبقة التخزين المشتركة بدون متغيرات عالمية.

5. تنسيق الاستجابة الموحد

تُرجع جميع نقاط النهاية نفس بنية JSON {success, data, error}، مما يعني أن العملاء يحتاجون إلى التحليل مرة واحدة فقط لتحديد ما إذا كان الطلب ناجحًا.

❓ أسئلة شائعة

س1: لماذا نستخدم sync.RWMutex بدلاً من sync.Mutex؟

يسمح Mutex فقط لـ goroutine واحد بالوصول في كل مرة، بينما يسمح RWMutex بعدة عمليات قراءة بالتنفيذ المتزامن — فقط عمليات الكتابة تقفل بشكل حصري. بالنسبة لسيناريوهات "القراءة الثقيلة، الكتابة الخفيفة" (مثل استعلامات API التي تفوق التعديلات بكثير)، يمكن أن يحسّن RWMutex بشكل كبير الأداء المتزامن.

س2: هل يجب استخدام هذا الموجه في الإنتاج؟

غير موصى به. هذا الموجه هو تنفيذ مبسط لأغراض تعليمية، يفتقر إلى دعم المطابقة النمطية، والبطاقات البرية، والكشف التلقائي عن الأسلوب، وميزات متقدمة أخرى. للإنتاج، استخدم مكتبات توجيه ناضجة مثل chi أو gorilla/mux أو التوجيه المحسن المدمج في Go 1.22+.

س3: بيانات التخزين في الذاكرة تُفقد بعد إعادة التشغيل. كيف نحل هذا؟

يستخدم هذا الدرس التخزين في الذاكرة للتركيز على جزء REST API. في المشاريع الحقيقية، يجب عليك الحفاظ على البيانات في قاعدة بيانات (مثل SQLite أو PostgreSQL أو MongoDB). الدرس التالي سيغطي كيفية تشغيل قواعد البيانات باستخدام Go.

س4: هل ترتيب تنفيذ الوسائط مهم؟

نعم. الاستعادة تذهب إلى الخارج لضمان أنها تلتقط جميع عمليات الذعر الداخلية؛ المصادقة تذهب قبل التسجيل حتى لا تُنشئ طلبات غير مصادق سجلات أعمال (على الرغم من أنك يمكنك القيام بالعكس لتسجيل جميع الطلبات بما فيها غير المصادقة). اضبط بشكل مرن بناءً على احتياجات العمل.

📖 ملخص

في هذا الدرس طبقنا بشكل شامل عدة نقاط معرفية تعلمنها سابقًا لبناء REST API كامل من الصفر:

هذه الأنماط هي أساس تطوير الويب في Go. بعد إتقانها، يمكنك بسهولة التوسع إلى تكامل قواعد البيانات، ومصادقة JWT، وتحديد معدل API، وغيرها من السيناريوهات المعقدة.

📝 تمارين

التمرين 1: إضافة دعم التصفح

عدّل نقطة نهاية GET /api/todos لدعم معاملات الاستعلام ?page=1&size=10 لنتائج مصفحة. تلميح: استخدم r.URL.Query() لقراءة المعاملات وقطع نتائج GetAll.

التمرين 2: إضافة وسائط CORS

اكتب CORSMiddleware يضيف ما يلي إلى رؤوس الاستجابة:

تعامل مع طلبات OPTIONS المسبقة بإرجاع 204 مباشرة. أضفه إلى سلسلة الوسائط.

التمرين 3: تنفيذ وظيفة البحث

أضف مسارًا جديدًا GET /api/todos/search?q=keyword ينفذ مطابقة غامضة للعنوان في التخزين في الذاكرة (باستخدام strings.Contains) ويرجع عناصر المهام المطابقة.


الدرس التالي: عمليات قواعد البيانات

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%