The Journey From Undocumented REST API to GraphQL
REST API לא מתועד הוא חוב שמתרבה. ספי ניניו מראה איך למפות requests ו-responses, לייצר Swagger ולבנות GraphQL בלי rewrite של backend קיים ומורכב.
צפו בהרצאההסיפור שמאחורי ההרצאה
- REST משותף למוצרים שונים עלול להחזיר payload גדול ולא רלוונטי לכל צרכן.
- GraphQL נותן ל-client לבקש בדיוק את השדות שהוא צריך, אבל לא מבטל עבודה בצד השרת.
- rewrite ל-TypeScript או תיעוד ידני ב-Swagger היו יקרים מדי עבור API ותיק ועשיר.
- middleware שתופס request ו-response בזמן בדיקות יכול ליצור OpenAPI חי מהשימוש האמיתי במוצר.
- הפתרון נועד לשלב מיפוי, לא לרוץ בפרודקשן או להחליף בעלות על schema.
REST API לא מתועד לא נשאר "רק לא מתועד". ככל שמצטברים clients, routes ולוגיקה, הוא הופך למקום שאף אחד לא רוצה לגעת בו. ספי ניניו מציג מקרה שבו מוצר web, desktop ומובייל דיברו עם אותו backend, אבל כל אחד היה צריך data אחר. הפתרון לא היה rewrite הרואי. הוא התחיל במיפוי של מה שהמערכת באמת עושה.
כשאותו API מחזיר יותר ממה שהמוצר צריך
רופא ומטופל עשויים להסתכל על אותו לוח זמנים, אבל לא צריכים אותו response. הרופא רוצה לראות פגישות, מטופלים וזמינות. המטופל רוצה רק slot פנוי. REST משותף יכול להחזיר לשניהם אותו payload, עם שדות שהצד השני לא צריך. זה לא רק עניין אסתטי. לא תמיד המשתמש נמצא על חיבור מצוין, ו-data מיותר פוגע בחוויה.
GraphQL נותן ל-client query שבו הוא בוחר את השדות. אפשר לבקש title של post ושם של author, בלי לקבל tags, likes, כתובת או כל השדות האחרים. זה גם מצמצם את רצף הבקשות מצד ה-client. GraphQL אינו קסם שמעלים את העבודה: השרת עדיין צריך לפתור קשרים ולדבר עם services. אבל הוא משנה מי מגדיר את צורת התשובה.
למה המיגרציה נתקעה
ה-backend היה Node.js בלי TypeScript, עם routes רבים, לוגיקה קיימת וסכמת Mongoose עשירה ב-refs וב-virtual values. כתיבת GraphQL schema ידנית מהתחלה לא הייתה ריאלית. גם תיעוד ידני של כל endpoint ב-Swagger דרש מאמץ עצום. מיפוי ישיר מה-database נשמע מפתה, אך מפספס את שכבת ה-API ומעביר ל-client לוגיקה שלא אמורה לזלוג אליו.
GraphQL Mesh מול Mongoose גם לא עבד היטב במבנה הזה. וזה לא היה מספיק טוב אפילו אם היה עובד: database schema לא שווה API contract. לכן ספי מחפש מקור אחר - לא הקוד התיאורטי ולא הדאטאבייס, אלא ה-requests וה-responses שהמוצר משתמש בהם בפועל.
לתעד דרך שימוש אמיתי
הטריק הוא middleware ב-Express. כשהוא פעיל, הוא שומר את res.json המקורי, עוטף אותו בפונקציית analyze, מקבל את ה-request ואת ה-response, ואז קורא למימוש המקורי. כך הוא לא רק רואה את הבקשה - הוא רואה גם את התשובה, status code, body, query params ו-path params.
QA או E2E מריצים את המוצר דרך ה-use cases שלו. הקוד מייצר Swagger/OpenAPI מתוך מה שנצפה. התוצאה חזקה דווקא בגלל מה שאין בה: endpoints שאף אחד לא משתמש בהם אינם מקבלים מקום רק כי מישהו מפחד למחוק אותם. אחר כך GraphQL Mesh מקבל את Swagger ומייצר schema. קובץ תיעוד שהיה בלתי אפשרי לכתוב ידנית הופך לקלט שנוצר מההתנהגות.
schema לא צריך לשכפל את עצמו
בדמו של translations, response של list, create ו-delete יכול להפנות לאותו type של Translation. אבל input ל-create אינו אותו type: למשל, אין לו ID. מעבר לזה, reuse אגרסיבי קושר בין input ל-response ובין mutations שונות. כשאחד משתנה, האחרים משתנים בלי שהתכוונתם. הסכמה צריכה לשקף גבולות עסקיים, לא רק לחסוך שורות.
גם GraphQL endpoint יחיד אינו אומר שהכל מגיע מאותה טבלה. resolvers יכולים להביא data ממקורות שונים ולחבר בין post ל-author. ה-client פשוט מבקש את מה שהוא צריך. זו הסיבה שהמסע מתחיל בחוזה API ותיעוד, ולא ברצון להחליף פרוטוקול.
לא פתרון לפרודקשן, כלי שמאפשר להגיע לשם
ספי מדגיש שהקוד שהציג הוא כלי מיפוי, לא middleware שרוצים להשאיר פעיל בפרודקשן. הוא נועד לפאזה מסוימת: להפעיל flag, להריץ בדיקות, לבנות baseline של ה-API ואז להחליט על תחזוקה. אפשר לעדכן schema כחלק מפיתוח פיצ'ר, או לתת ל-QA ול-CI/CD לשמור אותו עדכני.
מה לוקחים למערכת הוותיקה הבאה
הפתרון של ספי עובד כי הוא מתייחס לבדיקות לא רק כשומר סף, אלא כמקור מידע. צוות QA כבר יודע להפעיל מסכים, למלא טפסים ולבדוק תרחישים. E2E tests כבר מכירים את הנתיבים החשובים של המוצר. במקום לבקש מצוות backend לשבת מול רשימת routes אינסופית ולנחש מה חוזר מכל אחד, נותנים לשימוש האמיתי לחשוף את החוזה.
זה גם משנה את סדר העבודה. לא מתחילים בשאלה אם GraphQL הוא הדבר הכי מודרני. מתחילים בשאלות הפחות זוהרות: אילו endpoints קיימים? אילו מהם חיים? איזה status codes ותשובות הם מחזירים? אילו שדות חשופים ללקוחות, ואילו שדות קיימים רק ב-database? ברגע שיש תשובות, בחירת שכבת GraphQL היא כבר החלטה הנדסית שאפשר לבדוק, ולא אמונה בפרוטוקול.
יש כאן גם tradeoff ברור. תיעוד שנבנה מ-QA טוב רק כמו הכיסוי של QA. אם test לא מפעיל תרחיש, הוא לא יופיע ב-Swagger. לכן אין מקום להעמיד פנים שה-schema שנוצר הוא אמת מוחלטת. הוא baseline שמאפשר לצוות להתקדם, לזהות API מת, ולהתחיל לתחזק חוזה. משם אפשר להוסיף types מפורשים, לתקן שמות, להגדיר input נכון ולשים ownership על השינויים החדשים.
והדבר החשוב ביותר: לא משכפלים לוגיקה רק כדי לקבל API יפה יותר. GraphQL אמור לקרוא לשכבות שכבר מכירות את החוקים העסקיים. אם migration שולח את החוקים לקליינטים, הוא לא מפשט את המערכת - הוא מפזר אותה. החוזה החדש צריך לתת ל-client גמישות בשדות ובצורת query, בלי לוותר על השירותים שכבר יודעים לעשות את העבודה.
זו גם סיבה טובה להפריד בין פעולת המיפוי לבין הפעלת GraphQL מול משתמשים. קודם אפשר ליצור Swagger ולבדוק אותו מול הצוותים. אחר כך אפשר להרים playground, לנסות queries ולוודא שה-resolvers מחזירים את מה שה-client צריך. רק אחרי שהחוזה ברור עוברים את השכבה הבאה: עדכון הלקוחות ל-Apollo Client או לכל client אחר. כך לא מחליפים API ותיק ביום אחד, ולא דורשים מהפרונט להמר על schema שעוד לא נבדק.
במילים אחרות, migration טוב אינו מחיקת REST. הוא יצירת מסלול בטוח לידו: מיפוי, תיעוד, schema, אימות מול שימוש אמיתי ואימוץ הדרגתי. אם משהו חסר, יודעים איזה request צריך להריץ. אם משהו מיותר, יודעים למה אפשר להיזהר לפני שמוחקים. זה הרבה פחות דרמטי מ-rewrite, והרבה יותר שימושי לצוות שמחזיק מוצר חי.
הערך הוא לא רק GraphQL. זו דרך לפרק חוב טכני בלי לעצור הכול. מודדים התנהגות, מתעדים את מה שבאמת קיים, ורק אז בונים שכבה חדשה. להמשך על כלים ויזואליים וחוזה רכיבים, האזינו גם ל-פרק 62 על Storybook. קודם מגלים את המערכת. אחר כך משנים אותה.
"אני יכול לפתח את ה-API פעם אחת ועדיין להיות יעיל בפיילוד שחוזר לכל אחד מהצרכנים."
- ספי ניניו
"אין סיכוי"
- ספי ניניו
"אין לי את מה שלא משתמשים בו."
- ספי ניניו
"זה לא פתרון של עד זה פרודקשן."
- ספי ניניו
- 00:00:00 שני clients, API אחד ופיילוד מיותרספי מציג backend משותף למוצר web, desktop ומובייל, ואת הצרכים השונים של רופאים ומטופלים.
- 00:03:56 מה GraphQL נותן ל-clientquery יחיד מאפשר לצרכן לבקש רק את השדות שהוא צריך ולצמצם העברת data מיותר.
- 00:07:48 למה לא לכתוב schema ידניתNode.js ללא types, routes עשירים וסכמת Mongo מורכבת הפכו migration ידני למאמץ שלא היה נכנס לטיימליין.
- 00:10:00 שלוש דרכים שלא הספיקוTypeScript, Swagger ידני ומיפוי ישיר של Mongo נבחנו, אבל האחרון גם היה מאבד את לוגיקת ה-API.
- 00:14:00 לתעד לפי requests אמיתייםהפתרון תופס את ה-request ואת ה-response בזמן הרצת QA או E2E ומייצר מהם Swagger.
- 00:18:00 מ-Swagger ל-GraphQL Meshברגע שה-OpenAPI קיים, GraphQL Mesh מייצר schema ותיעוד שמבוססים על מה שהמוצר באמת משתמש בו.
- 00:24:00 types, references ו-inputsהדמו מראה איך responses חולקים type, ולמה input ו-response לא צריכים להיות אותו אובייקט.
- 00:30:00 הטריק של Express middlewareעוטפים את res.json, שומרים את ה-response, מנתחים אותו ואז קוראים ליישום המקורי כדי לא לשנות את ההתנהגות.
- 00:36:00 QA, CI/CD ותחזוקת schemaהצוות יכול למפות שימוש דרך אוטומציה, לעדכן schema בפיצ'ר או להכניס את התהליך ל-CI/CD.
מה כוסה בהרצאה
שאלות מההרצאה
למה לעבור מ-REST API ל-GraphQL?
במקרה של ספי, כמה clients קיבלו מאותו REST API data שלא היה נחוץ להם. GraphQL מאפשר ל-client לבקש שדות ספציפיים ב-query אחד. זה יכול לצמצם payload ולפשט את ה-client, אך השרת עדיין צריך לבצע resolving ולעבוד מול המקורות הרלוונטיים.
למה לא למפות GraphQL ישירות מ-MongoDB?
מיפוי כזה עלול להחמיץ את לוגיקת ה-API שכבר נמצאת ב-routes וב-services. בנוסף, במקרה שהוצג, handler מול סכמת Mongoose מורכבת לא תמך היטב ב-refs וב-virtual values. ה-API הקיים, ולא רק ה-database, הוא מקור האמת של ההתנהגות.
איך יוצרים Swagger ל-API לא מתועד?
אפשר להריץ את המוצר דרך QA או E2E, לתפוס request ו-response ולנתח מהם path params, query params, body, status codes ומבנה response. כך מתעדים use cases שבאמת קיימים, במקום לנחש ידנית את כל ה-routes ואת ה-endpoints שכבר אינם בשימוש.
האם middleware כזה צריך לרוץ בפרודקשן?
לא. ספי מציג אותו ככלי מיפוי עם flag שמפעיל אותו רק במצב ייעודי. המטרה היא לאסוף תיעוד וסכמה בזמן בדיקות או פיתוח. אחר כך בוחרים אם לעדכן את ה-schema ידנית מתוך פיצ'ר או לתחזק אותו כחלק מ-CI/CD.