אחד מהדברים שאנחנו שואפים אליהם כאשר אנו בונים מערכת הוא סטנדרטיזציה. כלומר לעבוד לפי סטנדרטים אחידים. כך למשל, מתכנת שיצטרך להתממשק למערכת שלנו או לחלופין יצטרף לצוות שלנו יוכל לעשות זאת בקלות.
לטעמי ולדעתי, חלק גדול מהבשלות והבגרות כמתכנת היא להגמל מהצורך של להמציא את הגלגל מחדש ולעבוד עם סטנדרטים וחלק מהסטנדרטים של 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 כדי להציג אותו, למשל אני רוצה להוסיף מתודה נוספת ורוצה להשתמש במודל בינה מלאכותית כלשהו שיעשה לי חיים קלים או לבנות את הקליינט.
אז איך נראה התיאור שלו? מאד דומה לקובץ הדוגמה שהראינו. אנו נעבור על החלקים השונים:
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 ואיך הסטנדרט נראה, אין תירוץ שלא לעבוד איתו.
4 תגובות
או להשתמש ב SOAP (או גלגול מחודש שלו gRPC)
אני לא חושב ש-SOAP הוא בחירה לגיטימית לאפליקציות מודרניות ולא סתם הוא, וגם XML בכללי, פשוט לא קיים באפליקציות מודרניות (לא כולל משרדי ממשלה).
gRPC גם בעייתי לשימוש בגלל התמיכה שלו ב-HTTP2 והוא גם פחות נמצא באפליקציות שמחצינות API. למרות שיש לו יתרונות, קשה לראות אותו באפליקציות חיצוניות.
REST טוב לממשקים "פשוטים" ו"קטנים" , ברגע שאתה נכנס לצורך לתעד ממשק API , סימן שהממשק כבר מסובך מדיי ועדיף היה להשתמש ב SOAP שיש לו את WSDL המובנה וגם הקליינט נוצר בקלות – לא טרנדי ? אולי , אבל עדיף.
אני לא מסכים בכלל בנוגע לממשקים פשוטים וקטנים (והאמת היא שבעולם המיקרוסרוויסים אנחנו בבעיה אחרת אם הממשקים שלנו ארוכים ומסורבלים). אבל אני לא חושב שיצא לי לפתח ב-SOAP בעשר השנים האחרונות, אולי בגלל שעבדתי בסקייל גבוה. שם ה-overhead של ה-XML היה הורג אותנו ולא היה שום יתרון למבנה הסדור של XML.
בכל מקרה, למרות יתרונותיו של ה-SOAP, מאד קשה למצוא אותו ב-APIים פומביים, למעט אולי כאלו של ממשלת ישראל וכיוון שמאמר זה מניח תשתית לעבודה עם ChatGPT Agents, אני לא חושב שאכתוב עליו אי פעם.