openAPI

שימוש בתשתית הפופולרית למיפוי ותיעוד של API וגם הסבר בסיסי על מה זה API
צילום מסך של סוואגר

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

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

מה זה API?

ראשית, הסבר קצרצר על מה זה API? אני יודע שזה בסיסי אבל אולי יש מישהו שלא יודע ואתאר זאת בשבילי. בגדול זה ראשי תבות של Application Programming Interface – וזה אומר ממשק של תוכנה, במקרה שלנו מערכת וובית. למשל ויקיפדיה. אם אני רוצה מידע מויקיפדיה על עמוד מסוים, אני יכול להתחבר ל-API שלהם – לשגר בקשה עם הכותרת של העמוד ולקבל את המידע עליו בפורמט JSON נוח לקריאה. הנה למשל דוגמה לקבלת מידע על הערך שלי בויקיפדיה:

https://he.wikipedia.org/w/api.php?action=query&prop=info&titles=רן בר-זיק

אם תריצו את זה תקבלו את ערך ה-JSON של העמוד הזה. עם API מסוג REST, שזה רוב ה-API שיש, אנחנו משתמשים גם במתודות על מנת לקבל, לשלוח, למחוק או לעדכן מידע.

הגדרת API

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

ואיך אנחנו מגדירים API? בשביל זה יש לנו את openAPI שזו דרך להגדיר ולתאר API ב-JSON או ב-YAML,

כדי להמחיש איך נראה תיאור כזה, אני אתן דוגמה נחמד עם YAML:

openapi: 3.0.3
info:
  version: 1.0.0
  title: my things API
  description: An API of my things
paths:
  /list:
    get:
      description: Returns a list of my things              
      responses:
        '200':
          description: Successful response

מה שטוב ב-YAML שהוא קל לקריאה כמו JSON. במקרה הזה מאד קל להבין מייד, אפילו על ידי אדם, מה בדיוק יש פה. יש את שם ה-API והתיאור שלו וכן את רשימת הנתיבים לקבלת/שליחת המידע. אם אני שולח בקשת GET אל list/ אני אקבל קוד של 200 ואת רשימת הדברים שלי.

עכשיו נכנס קצת יותר עמוק. בואו ונתאר API אחר. של אתר עם רשימת מצרכים. על מנת להמחיש את העניין, נתחיל דווקא עם הקוד שלו ואני אדגים גם בפייתון וגם בג׳אווהסקריפט, דלגו לשפה אתם יותר מבינים (אפשר באינסוף שפות, openAPI היא אגנוסטית).

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

ליצור פריט באמצעות שליחת בקשת POST אל הכתובת /api/items/ עם אובייקט JSON בשם item: ItemName כאשר ה-itemName הוא השם של המוצר.
למחוק פריט באמצעות שליחת בקשת DELETE אל הכתובת api/items/itemName. גם פה ה-itemName הוא השם של המוצר.
לקבל את שמות המוצרים באמצעות שליחת בקשת GET אל הכתובת /api/items/ ולקבל מערך של אובייקטים עם שמות המוצרים.

אני אדגים באמצעות POSTMAN (מקווה שכולם מכירים את הכלי השימושי הזה) כיצד אני עובד עם ה-API:

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

דוגמת ג׳אווהסקריפט לשרת שמייצר API של רשימת פריטים

import express from 'express';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded


let groceryItems = []; // In-memory database

// Get all grocery items
app.get('/api/items', (req, res) => {
    res.json(groceryItems);
});

// Create a new grocery item
app.post('/api/items', (req, res) => {
    const item = req.body;
    if (!item.name) {
        return res.status(400).send('Item name is required');
    }
    groceryItems.push(item);
    res.status(201).send('Item added');
});

// Delete a grocery item by name
app.delete('/api/items/:itemName', (req, res) => {
    const itemName = req.params.itemName;
    groceryItems = groceryItems.filter(item => item.name !== itemName);
    res.status(204).send();
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

כדי להריץ אותו אצלכם, פשוט יוצרים תיקיה, מקלידים npm init ומכניסים את הטקסט שלעיל ל index.mjs מתקינים את express עם npm i express ומריצים עם node index.mjs.

דוגמת פייתון לשרת שמייצר API של רשימת פריטים

from fastapi import FastAPI, HTTPException
from typing import Optional
from pydantic import BaseModel

app = FastAPI()

class GroceryItem(BaseModel):
    name: str

grocery_items = []  # In-memory database

# Get all grocery items
@app.get("/api/items")
async def get_items():
    return grocery_items

# Create a new grocery item
@app.post("/api/items", status_code=201)
async def create_item(item: GroceryItem):
    if not item.name:
        raise HTTPException(status_code=400, detail="Item name is required")
    grocery_items.append(item.dict())
    return "Item added"

# Delete a grocery item by name
@app.delete("/api/items/{item_name}", status_code=204)
async def delete_item(item_name: str):
    global grocery_items
    grocery_items = [item for item in grocery_items if item["name"] != item_name]
    return "Item deleted"

# Start the server
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

על מנת להריץ אותו, צרו תיקיה, הקלידו poetry init, התקינו שם את uvicorn ואת fastapi בגרסאות ה-latest שלהן. הדביקו את הקובץ בשם grocery_api.py. כתבו poetry install והריצו עם
uvicorn grocery_api:app –reload

יופי, אז יש לנו API. למה לכל הרוחות אני צריך לתאר אותו? מכמה סיבות – למשל אני רוצה לשים אותו ב-Swagger כדי להציג אותו, למשל אני רוצה להוסיף מתודה נוספת ורוצה להשתמש במודל בינה מלאכותית כלשהו שיעשה לי חיים קלים או לבנות את הקליינט.

דוגמה לתוכנת סוואגר שמתארת את ה-API שלנו

אז איך נראה התיאור שלו? מאד דומה לקובץ הדוגמה שהראינו. אנו נעבור על החלקים השונים:

openapi: 3.0.0
info:
  title: Grocery Service API
  version: 1.0.0
servers:
  - url: https://grocery-service.barzik.com/api
    description: Production server
  - url: http://localhost:3000/api
    description: Development server
paths:
  /items:
    get:
      summary: Get the list of all grocery items
      responses:
        '200':
          description: A list of grocery items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/GroceryItem'

    post:
      summary: Create a new grocery item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroceryItem'
      responses:
        '201':
          description: Grocery item created

  /items/{itemName}:
    parameters:
      - in: path
        name: itemName
        required: true
        schema:
          type: string
        description: The name of the grocery item

    delete:
      summary: Delete a grocery item by name
      responses:
        '204':
          description: Grocery item deleted

components:
  schemas:
    GroceryItem:
      type: object
      properties:
        name:
          type: string
          description: Name of the grocery item

הסבר על כל חלק וחלק

אתם יכולים לנתח את הקוד לבד כמובן. אבל אני מכניס כאן הסבר על כל חלק מה-YAML הנחמד הזה.

מטא מידע

החלק הראשון הוא מטא מידע של ה-API. הגרסה שלו, השם שלו וגרסת ה-openAPI שאנו משתמשים בה:

openapi: 3.0.0
info:
  title: Grocery Service API
  version: 1.0.0

שרתים

אנחנו יכולים לציין כמה שרתים לפנות אליהם או שרת אחד. פה בחרתי בשניים – שרת הפיתוח ושרת הפרודקשן:

servers:
  - url: https://grocery-service.barzik.com/api
    description: Production server
  - url: http://localhost:3000/api
    description: Development server

הנתיבים: ה-item ללא פרמטר

הנה החלק החשוב והמשמעותי, אנחנו מציינים את חלקי ה-API שלנו. החלק הראשון הוא items – זה החלק של ה-api/items שאין שום דבר אחריו – אנחנו שולחים אליו רק GET ו-POST (ה-DELETE נשלח ל items/itemName ולא רלוונטי אלינו כרגע).

paths:
  /items:

פירוט נתיב: GET

זה החלק שבו אנו מקבלים את כל המידע. אז אפשר לראות שיש תיאור בשפה חופשית, תגובה אפשרית אחת (ת׳כלס יש רק אחת) זו 200. התיאור שלה גם בשפה חופשית אבל מה התוכן שלה? מה פורמט הנתונים? זה מפורט בחלק של הcontent. ראשית ה-encoding, איך נראה המידע ואז הסכמה – תיאור המידע.

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

    get:
      summary: Get the list of all grocery items
      responses:
        '200':
          description: A list of grocery items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/GroceryItem'

פירוט נתיב: POST

החלק הבא הוא תיאור של ה-POST. גם פה יש רפרנס של המידע שאנחנו אמורים לקבל. אימה וזעם – הסכמה היא גם הפניה ל… אותו חלק במסמך 😱 עוד שניה נגיע אליו שוב.

    post:
      summary: Create a new grocery item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroceryItem'
      responses:
        '201':
          description: Grocery item created

הנתיבים: ה-item עם פרמטר

הנתיב הבא הוא אותו items, אבל הפעם זה עם הפרמטר. למה? כי זה DELETE שעובד עם פרמטר. פה יש בנתיב עצמו יש תיאור של הסכימה של הפרמטר itemName – מה הוא, מי הוא וגם תיאורים חופשיים. יש פה גם את הנתיב של ה-delete שהוא היחידי שנמצא פה.

  /items/{itemName}:
    parameters:
      - in: path
        name: itemName
        required: true
        schema:
          type: string
        description: The name of the grocery item

    delete:
      summary: Delete a grocery item by name
      responses:
        '204':
          description: Grocery item deleted

הסכמה – החלק האחרון והמפחיד

יש סיכוי שזה יישמע קצת מוגזם, אבל לי תמיד החלק הזה היה הכי מפחיד ומוזר – החלק שבו מתארים את סכמת הנתונים. למה זה מלחיץ אותי? אולי בגלל שמפנים אליו ממקומות אחרים והדולרים המוזרים בהפניות. אבל כשמסתכלים עליו כך, הוא לא נראה מפחיד במיוחד וכולל רק את שם האובייקט (שאליו נוכל להפנות ממקומות אחרים שמשתמשים בו) ואיך הוא נראה.

components:
  schemas:
    GroceryItem:
      type: object
      properties:
        name:
          type: string
          description: Name of the grocery item

אז אחרי שיצרנו קובץ openAPI, מה עושים איתו?

אפשר לעשות עם ה-openAPI הזה לא מעט דברים, אבל בואו ונראה את הדוגמה הפשוטה והאינטואיטיבית ביותר, פשוט להכנס ל-editor.swagger.io ולהדביק את ה-YAML המלא. ישר תראו איך הדוקומנטציה האולטרא מגניבה שלו ואיך זה נראה כל כך ברור איך משתמשים ב-API הזה. אם השרת חי, אפשר אפילו לשגר בקשות אליו ולראות את התגובה – הוא גם נותן דוגמאות curl להרצה מהטרמינל.

אלכס גלמן, ארכיטקט תוכנה בכיר וקולגה שלי בסייברארק, פרסם ממש עכשיו פוסט באנגלית שמראה איך מחברים ChatGPT ל-API משלכם ועושים את זה עם openAPI.

יותר מזה, דרך swagger אפשר ליצור ישר קליינט שמשתמש ב-API. אבל למה להזיע? תדביקו את קובץ ה-YAML הזה בקופיילוט צ׳אט ותבקשו ממנו לבנות קליינט אליו.

זו היתה הקדמה, שלא נעים לי לומר שכתבתי אותה רק בגלל מאמרי המשך שבהם הבנתי שלא כולם מכירים את openAPI – אבל יש לא מעט דברים נוספים לדעת על הסכמות של openAPI – איך מייצרים למשל חלקים שאפשר להשתמש בהם שוב וכמובן תיאור יותר מפורט של אובייקטים וסכמות. אבל, וזה אבל חשוב, כל הדברים האלו ניתנים לחיפוש בגוגל ובדוקומנטציה – עכשיו כשאתם יודעים שיש openAPI ואיך הסטנדרט נראה, אין תירוץ שלא לעבוד איתו.

פוסטים נוספים שכדאי לקרוא

גלילה לראש העמוד