דלג לתוכן הראשי

פיתוח API ב-Laravel - מה למדנו אחרי 5 שנים בפרודקשן

לקחי API מ-5 שנים בפרודקשן: אימות, webhooks, וולידציה, rate limiting, וטיפול בשגיאות. מה עובד ומה היינו עושים אחרת.

אייל גנץ אייל גנץ
|
3 דקות קריאה
קוד Laravel API עם middleware אימות

הנה מה שהיינו עושים אחרת אם היינו מתחילים היום - ומה שהיינו שומרים בדיוק אותו דבר.

הארכיטקטורה ששרדה

בהתחלה עשינו over-engineering. שירותים קוראים לשירותים קוראים ל-repositories. הפשטות להפשטות.

אחרי חמש שנים ושינויים אינספור, הנה איך נראית הארכיטקטורה שנבחנה בקרב:

// פשוט. ישיר. ניתן לתחזוקה.
class OrderController extends Controller
{
    public function store(StoreOrderRequest $request): JsonResponse
    {
        $order = Order::create([
            'customer_id' => $request->customer_id,
            'status' => OrderStatus::PENDING,
            'notes' => $request->notes,
        ]);

        foreach ($request->items as $item) {
            $order->items()->create($item);
        }

        event(new OrderCreated($order));

        return response()->json([
            'data' => new OrderResource($order->load('items', 'customer')),
        ], 201);
    }
}

ללא service class. ללא repository. רק controller שיוצר הזמנה ויורה event. ה-event מטפל ב-side effects (התראות, עדכוני מלאי, סנכרון).

הלקח: אל תפשיטו עד שיש סיבה. הסרנו כ-40% משכבת ה-service שלנו לאורך חמש שנים. היא הוסיפה מורכבות ללא ערך.

אימות - מה עשינו נכון

אנחנו משתמשים ב-Laravel Sanctum עם גישה מדורגת:

class ApiAuthMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        // מפתח API למערכות חיצוניות
        if ($request->hasHeader('X-API-Key')) {
            return $this->handleApiKey($request, $next);
        }

        // Token לאפליקציות פנימיות (פאנל ניהול, מובייל)
        if ($request->bearerToken()) {
            return $this->handleBearerToken($request, $next);
        }

        return response()->json(['error' => 'Unauthorized'], 401);
    }

    private function handleApiKey(Request $request, Closure $next): Response
    {
        $key = $request->header('X-API-Key');
        $hashedKey = hash('sha256', $key);

        $client = ApiClient::where('key_hash', $hashedKey)
            ->where('is_active', true)
            ->first();

        if (!$client) {
            return response()->json(['error' => 'Invalid API key'], 401);
        }

        // תיעוד שימוש ב-API ל-rate limiting ודיבוג
        $client->logRequest($request);
        $request->setApiClient($client);

        return $next($request);
    }
}

למה זה עובד:

  • מפתחות API ל-webhooks ואינטגרציות חיצוניות
  • Bearer tokens למשתמשים מאומתים (פאנל ניהול)
  • הפרדה ברורה בין אימות מכונה-למכונה לאימות משתמש
  • תיעוד בקשות לדיבוג - זה הציל אותנו פעמים רבות

טיפול ב-Webhooks

רוב הטוטוריאלים מדלגים על webhooks. הנה מה שלמדנו מטיפול ב-webhooks מ-Shopify, Stripe ושותפים שונים:

class WebhookController extends Controller
{
    public function shopify(Request $request): Response
    {
        // 1. מאמתים חתימת webhook קודם
        if (!$this->verifyShopifySignature($request)) {
            Log::warning('Invalid Shopify webhook signature', [
                'ip' => $request->ip(),
                'headers' => $request->headers->all(),
            ]);
            return response()->json(['error' => 'Invalid signature'], 401);
        }

        // 2. שומרים את ה-payload הגולמי מיד (לפני כל עיבוד)
        $webhook = WebhookLog::create([
            'source' => 'shopify',
            'topic' => $request->header('X-Shopify-Topic'),
            'payload' => $request->all(),
            'processed' => false,
        ]);

        // 3. שולחים לתור לעיבוד אסינכרוני
        ProcessShopifyWebhook::dispatch($webhook);

        // 4. מחזירים 200 מיד (Shopify מצפה לתגובה מהירה)
        return response()->json(['received' => true], 200);
    }

    private function verifyShopifySignature(Request $request): bool
    {
        $hmac = $request->header('X-Shopify-Hmac-SHA256');
        $data = $request->getContent();
        $calculated = base64_encode(
            hash_hmac('sha256', $data, config('shopify.webhook_secret'), true)
        );

        return hash_equals($calculated, $hmac);
    }
}

עקרונות מפתח:

  1. מאמתים חתימות לפני עיבוד
  2. שומרים payload גולמי מיד - מכרה זהב לדיבוג
  3. מעבדים אסינכרונית דרך תור - מחזירים 200 מהר
  4. מתעדים כשלים ומתריעים על דפוסים

וולידציה - היו פרנואידים

כללי הוולידציה שלנו מחמירים. מאוד מחמירים:

class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'customer_id' => [
                'required',
                'integer',
                Rule::exists('customers', 'id')->where('is_active', true),
            ],
            'items' => ['required', 'array', 'min:1', 'max:50'],
            'items.*.product_id' => [
                'required',
                Rule::exists('products', 'id')->where('is_active', true),
            ],
            'items.*.quantity' => ['required', 'integer', 'min:1', 'max:10'],
            'items.*.customizations' => ['nullable', 'array'],
            'items.*.customizations.engraving' => ['nullable', 'string', 'max:50'],
            'shipping_address' => ['required', 'array'],
            'shipping_address.line1' => ['required', 'string', 'max:100'],
            'shipping_address.city' => ['required', 'string', 'max:50'],
            'shipping_address.country' => ['required', 'string', 'size:2'],
            'expected_delivery' => ['nullable', 'date', 'after:today'],
        ];
    }
}

למה וולידציה פרנואידית חשובה:

  • תפסנו באג שבו Shopify שלח מערכים ריקים במקום null
  • אינטגרציית צד שלישי ניסתה לשלוח 500+ פריטים בהזמנה אחת (התקפת בוט)
  • קודי מדינה לא תקינים שברו את חישוב המשלוח שלוש פעמים לפני שהוספנו size:2

טיפול בשגיאות

סטנדרטיזנו תגובות שגיאה מוקדם. אותו פורמט בכל מקום:

// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (Throwable $e, Request $request) {
        if (!$request->expectsJson()) {
            return null; // תנו ל-Laravel לטפל בשגיאות web
        }

        // שגיאות מוכרות עם הודעות בטוחות
        if ($e instanceof ModelNotFoundException) {
            return response()->json([
                'error' => [
                    'code' => 'RESOURCE_NOT_FOUND',
                    'message' => 'המשאב המבוקש לא נמצא.',
                ],
            ], 404);
        }

        if ($e instanceof ValidationException) {
            return response()->json([
                'error' => [
                    'code' => 'VALIDATION_ERROR',
                    'message' => 'הנתונים שסופקו אינם תקינים.',
                    'details' => $e->errors(),
                ],
            ], 422);
        }

        // שגיאות לא ידועות - מתעדים ומחזירים הודעה גנרית
        Log::error('API Error', [
            'exception' => get_class($e),
            'message' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
            'request' => $request->all(),
        ]);

        return response()->json([
            'error' => [
                'code' => 'INTERNAL_ERROR',
                'message' => 'אירעה שגיאה בלתי צפויה.',
            ],
        ], 500);
    });
})

הכלל: לעולם אל תחשפו הודעות שגיאה פנימיות ללקוחות. תעדו הכל. החזירו פורמטים בטוחים ועקביים.

Rate Limiting - למדנו בדרך הקשה

לא היה לנו rate limiting בהתחלה. אז שרת staging של לקוח נתקע בלולאת retry ופגע ב-API שלנו 50,000 פעמים בשעה.

עכשיו יש לנו rate limiting מדורג:

RateLimiter::for('api', function (Request $request) {
    $client = $request->getApiClient();

    if (!$client) {
        // בקשות ללא אימות מקבלות מגבלות מחמירות
        return Limit::perMinute(10)->by($request->ip());
    }

    return match ($client->tier) {
        'internal' => Limit::none(), // השירותים שלנו
        'partner' => Limit::perMinute(500)->by($client->id),
        'standard' => Limit::perMinute(100)->by($client->id),
        default => Limit::perMinute(30)->by($client->id),
    };
});

לקוחות מעריכים לדעת את המגבלות שלהם לפני שהם מגיעים אליהן.

מה היינו עושים אחרת

  1. Event sourcing להזמנות מיום ראשון - שחזור היסטוריית הזמנות מהמצב הנוכחי הוא כאב. הוספנו תיעוד events מאוחר יותר, אבל להזמנות המוקדמות חסר פירוט.

  2. תיעוד API טוב יותר מוקדם יותר - אנחנו משתמשים ב-Scribe עכשיו, אבל לשנתיים הראשונות, התיעוד היה ב-Google Doc שתמיד היה מיושן.

  3. יותר integration tests, פחות unit tests - בדיקה שה-API מחזיר תגובות נכונות שווה יותר מבדיקת מתודות מבודדות.

התוצאות

אחרי 5 שנים:

  • 99.9% uptime
  • כ-500K בקשות בחודש
  • זמני תגובה מתחת ל-100ms לרוב ה-endpoints
  • אפס אירועי אבטחה משמעותיים

השורה התחתונה: הדפוסים המשעממים עובדים. הדפוסים החכמים מוסרים. הקוד הפשוט שורד.

אם אתם בונים API, התחילו פשוט. הוסיפו מורכבות רק כשיש ראיות שאתם צריכים אותה. ותעדו הכל.

רוצים לבנות API שירוץ שנים?

נשמח לדבר על אילו דפוסים ישרדו גם אצלכם.

אייל גנץ
נכתב על ידי

אייל גנץ

מייסד ומפתח ראשי

מומחה בפיתוח מערכות מסחר אלקטרוני ואוטומציה עסקית עם למעלה מ-10 שנות ניסיון בבניית פתרונות טכנולוגיים מותאמים אישית.

שיחת ייעוץ בחינם

רוצים ליישם משהו דומה בעסק שלכם? בואו נדבר על איך נוכל לעזור לכם להשיג תוצאות.

התחילו שיחה