עיקבו אחרינו בטוויטר! @nodepractices
✍️ תורגם על ידי הוד בואר
לקריאה בשפות נוספות: סינית,
צרפתית,
פורטוגזית,
רוסית,
פולנית,
יפנית,
באסקית (
ספרדית,
עברית,
קוריאנית ו
טורקית בתהליך! )
-
🛰 2023 edition is released soon: We're now writing the next edition, stay tuned?
-
✨ 89,000 stars: Blushing, surprised and proud!
-
🔖 New menu and tags: Our menu is collapsible now and includes
#tags
. New visitors can read#strategic
items first. Returning visitors can focus on#new
content. Seniors can filter for#advanced
items. Courtesy of the one and only Rubek Joshi -
French translation!1! : The latest translation that joins our international guide is French. Bienvenue
1. הנכם קוראים עשרות מאמרים של שיטות העבודה המומלצות ב Node.js - המאגר הזה הוא סיכום לא יסולא בפז של שיטות העבודה המומלצות ב Node.js , כמו כן הוא נעשה על בשיתוף פעולה.
2. זהו האוסף הגדול ביותר, והוא ממשיך לגדול כל שבוע - נכון לרגע זה, יש למעלה מ 100 שיטות עבודה מומלצות, המלצות ארכיטקטורה והמלצות סגנון כתיבה. נושאים חדשים ובקשות חדשות (PR's) מתווספים כל יום במטרה לשמור את התוכן מעודכן. אנחנו נשמח לראותכם תורמים לפה, בין אם לתקן שגיאות קוד, עזרה בתרגום, או להציע רעיונות מבריקים חדשים. ראו את המדריך לכתיבת הנחיות.
3. שיטות העבודה כוללות מידע נוסף - רוב הנקודות כוללות קישור 🔗לקריאה נוספת שמרחיב על ידי דוגמאות קוד, ציטוטים מבלוגים נבחרים ומידע נוסף.
לימדו איתי: כיועץ, אני נפגש עם קבוצות מכל העולם במגוון פעולות כמו סדנאות ומעבר על קוד. 🎉 לאחרונה פרסמתי את הקורס המתקדם לכתיבת בדיקות
1. מבנה הפרוייקט (6)
1.1 בנו את הפרוייקט לפי רכיבים עסקיים #strategic
#updated
1.2 חלוקת הרכיבים ל3 שכבות, שמירה על שכבת הווב בגבולותיה #strategic
#updated
1.3 עטפו כלים משותפים בחבילות, שקלו את הפצתם
1.4 השתמשו בקונפיגורציה עם משתני סביבה באופן מודע, מאובטח והיררכי #updated
1.5 שקלו את כל ההשלכות בעת בחירת מסגרת #new
1.6 השתמשו ב-TypeScript במידתיות ובצורה מושכלת #new
2. ניהול שגיאות (12)
2.1 השתמשו ב Async-Await או הבטחות לניהול שגיאות אסינכרוניות
2.2 הרחיבו את מבנה אוביקט השגיאה המובנה Error #strategic
#updated
2.3 הבחינו בין שגיאות קטסטרופליות לבין שגיאות תפעוליות #strategic
#updated
2.4 נהלו את השגיאות במרוכז ולא באמצעות כלי ביניים #strategic
2.5 תעדו את שגיאות ה-API באמצעות OpenAPI או GraphQL
2.6 הורידו את התהליך בצורה מסודרת כאשר זר בא לבקר #strategic
2.7 השתמשו ב-Logger מוכר ואמין כדי להגדיל את הקְרִיאוּת של השגיאות #updated
2.8 בידקו את תגובת המערכת לשגיאות על ידי שימוש בכלי הבדיקות האהוב עליכם #updated
2.9 גלו שגיאות וזמני השבתה על ידי שימוש בכלי APM
2.10 תפסו מקרים לא מטופלים של דחיות של הבטחות #updated
2.11 היכשלו מהר, ודאו את משתני הקלט באמצעות ספריה יעודית
2.12 תמיד המתינו לתשובה מההבטחות לפני שאתם מעבירים את התשובה הלאה כדי להימנע ממעקב חלקי #new
3. תבניות קוד וסגנון עיצוב (13)
3.1 השתמשו ב-ESLint #strategic
3.2 השתמשו בתוספים של Node.js שמרחיבים את ESLint #updated
3.3 התחילו בלוק של קוד עם סוגריים מסולסלים באותה השורה
3.4 הפרידו בין ההצהרות השונות בצורה תקנית
3.5 תנו לפונקציה שם
3.6 השתמשו במוסכמות קבועות במתן שמות למשתנים, לקבועים, לפונקציות ולמחלקות
3.7 העדיפו const על פני let. ניטשו את var
3.8 טענו מודולים בתחילה, ולא בקריאה לפונקציות
3.9 הגדירו כניסה מסודרת לספריה שלכם #updated
3.10 השתמשו באופרטור ===
3.11 השתמשו ב-Async Await, המנעו מ-callbacks #strategic
3.12 השתמשו בפונקציות חץ (=>)
3.13 הימנעו מהשפעות צדדיות מחוץ לפונקציות #new
4. בדיקות ובקרת איכות (13)
4.1 לפחות, כיתבו בדיקות API לרכיבים השונים #strategic
4.2 סווגו 3 חלקים במתן שם לכל בדיקה #new
4.3 חלקו את הבדיקות לפי תבנית ה-AAA #strategic
4.4 וודאו כי גרסת ה-Node אחידה #new
4.5 הימנעו מאתחול מידע גרעיני משותף, הגדירו לפי צורך של בדיקה #strategic
4.6 תייגו את הבדיקות #advanced
4.7 בידקו את רמת כיסוי הבדיקות שלכם, זה יעזור לזהות דפוסי בדיקות שגויים
4.8 Use production-like environment for e2e testing
4.9 שכתבו את הקוד באופן קבוע בעזרת כלי ניתוח סטטי
4.10 הדמיית תשובות של שרתי HTTP חיצוניים #new
#advanced
4.11 בדקו את פונקציות הביניים בנפרד
4.12 קבעו את הפורט בייצור, הגדירו אקראי לבדיקות #new
4.13 בידקו את חמשת התוצאות האפשריות #strategic
#new
5. עלייה לאוויר (19)
5.1. ניטור #strategic
5.2. הגדילו את יכולת הצפייה בעזרת לוגים איכותיים #strategic
5.3. האצילו כל מה שאפשר (לדוגמה gzip, SSL) לשירות נפרד #strategic
5.4. קיבוע תלויות
5.5. הבטיחו את זמינות המערכת בעזרת הכלי המתאים
5.6. השתמשו בכל מעבדי ה-CPU
5.7. תיצרו ‘maintenance endpoint’
5.8. גלו את הלא ידוע בעזרת מוצרי APM #advanced
#updated
5.9. כתבו את הקוד מותאם להתקנה
5.10. מדדו ושימרו את ניצול הזיכרון #advanced
5.11. Get your frontend assets out of Node
5.12. Strive to be stateless #strategic
5.13. Use tools that automatically detect vulnerabilities
5.14. Assign a transaction id to each log statement #advanced
5.15. Set NODE_ENV=production
5.16. Design automated, atomic and zero-downtime deployments #advanced
5.17. Use an LTS release of Node.js
5.18. Log to stdout, avoid specifying log destination within the app
5.19. Install your packages with npm ci #new
6. אבטחה (27)
6.1. Embrace linter security rules
6.2. Limit concurrent requests using a middleware
6.3 Extract secrets from config files or use packages to encrypt them #strategic
6.4. Prevent query injection vulnerabilities with ORM/ODM libraries #strategic
6.5. Collection of generic security best practices
6.6. Adjust the HTTP response headers for enhanced security
6.7. Constantly and automatically inspect for vulnerable dependencies #strategic
6.8. Protect Users' Passwords/Secrets using bcrypt or scrypt #strategic
6.9. Escape HTML, JS and CSS output
6.10. Validate incoming JSON schemas #strategic
6.11. Support blocklisting JWTs
6.12. Prevent brute-force attacks against authorization #advanced
6.13. Run Node.js as non-root user
6.14. Limit payload size using a reverse-proxy or a middleware
6.15. Avoid JavaScript eval statements
6.16. Prevent evil RegEx from overloading your single thread execution
6.17. Avoid module loading using a variable
6.18. Run unsafe code in a sandbox
6.19. Take extra care when working with child processes #advanced
6.20. Hide error details from clients
6.21. Configure 2FA for npm or Yarn #strategic
6.22. Modify session middleware settings
6.23. Avoid DOS attacks by explicitly setting when a process should crash #advanced
6.24. Prevent unsafe redirects
6.25. Avoid publishing secrets to the npm registry
6.26. 6.26 Inspect for outdated packages
6.27. Import built-in modules using the 'node:' protocol #new
7. ביצועים (2) (בתהליך ✍️)
7.1. Don't block the event loop
7.2. Prefer native JS methods over user-land utils like Lodash
8. דוקר (15)
8.1 Use multi-stage builds for leaner and more secure Docker images #strategic
8.2. Bootstrap using node command, avoid npm start
8.3. Let the Docker runtime handle replication and uptime #strategic
8.4. Use .dockerignore to prevent leaking secrets
8.5. Clean-up dependencies before production
8.6. Shutdown smartly and gracefully #advanced
8.7. Set memory limits using both Docker and v8 #advanced
#strategic
8.8. Plan for efficient caching
8.9. Use explicit image reference, avoid latest tag
8.10. Prefer smaller Docker base images
8.11. Clean-out build-time secrets, avoid secrets in args #strategic #new
8.12. Scan images for multi layers of vulnerabilities #advanced
8.13 Clean NODE_MODULE cache
8.14. Generic Docker practices
8.15. Lint your Dockerfile #new
אמ;לק: בסיס המערכת צריך לכלול תיקיות או מאגרים שמייצג בצורה הגיונית את המידול העסקי. כל רכיב מייצג תחום מוצר (כלומר הקשר מוגבל), למשל 'משתמשים', 'הזמנות', וכולי... כל רכיב מכיל את ה API, לוגיקה ומסד הנתונים שלו. מה המטרה של זה? כאשר יש סביבה עצמאית כל שינוי משפיע אך ורק על החלק הרלוונטי - העומס הנפשי, סיבוכיות הפיתוח והחשש מפריסה חדשה של הרכיב הרבה יותר קטן. כתוצאה מכך, מתכנתים יכולים לפתח הרבה יותר מהר. זה לא דורש בהכרח הפרדה פיזית ויכול להיות מושג גם בMonorepo או multi-repo.
my-system
├─ apps (components)
│ ├─ orders
│ ├─ users
│ ├─ payments
├─ libraries (generic cross-component functionality)
│ ├─ logger
│ ├─ authenticator
אחרת: כאשר מוצרים של מודולים או נושאים שונים מעורבבים יחד, ישנו סיכוי גבוה שתיווצר מערכת ספגטי בעלת צימוד גבוה. לדוגמה, בארכיטקטורה שבה 'מודול א`' קורא לשירות מ'מודול ב;', אין הפרדה ברורהבין המודולים השונים - כל שינוי בקוד עלול להשפיע על משהו אחר. עם הגישה הזאת , מתכנתים שצריכים להוסיף מוצר חדש למערכת יאבקו בה בהבנה על מה השינוי שלהם יכול להשפיע. כתוצאה מכך, הם חששו לשבור מודולים אחרים, וכל פריסה נהייתה איטית יותר ומסוכנת יותר.
🔗 לקריאה נוספת: בנייה לפי רכיבים
אמ;לק: כל רכיב צריך לכלול 'שכבות' - תיקייה יעודית עם אחריות משותפת: 'entry-point' איפה שחלקי השליטה נמצאים, 'domain' איפה שהלוגיקה נמצאת ו 'data-access'. העיקרון המנחה של הארכיטקטורות המובילות בשוק הוא להפריד את האחריות הטכנית (למשל: HTTP, DB ועוד) מהלוגיקה היעודית של המוצר כך שהמתכנתים יוכלו לקודד יותר תכולות בלי לדאוג לגבי ניהול תשתיות. השמה של כל שכבה בתיקייה יעודית, שידועה גם כ-מודל 3 השכבות (באנגלית) זאת הדרך הפשוטה להשיג את המטרה.
my-system
├─ apps (components)
│ ├─ component-a
│ ├─ entry-points
│ │ ├─ api # controller comes here
│ │ ├─ message-queue # message consumer comes here
│ ├─ domain # features and flows: DTO, services, logic
│ ├─ data-access # DB calls w/o ORM
אחרת: לעתים דחופות נתקלים בכך שהמתכנתים מעבירים אובייקטי תקשורת כדוגמת request/reqponse לפונקציות בשכבות של הלוגיקה או ניהול המידע - דבר זה פוגע בעיקרון ההפרדה וגורם לכך שבעתיד יהיה קשה יותר להנגיש את הלוגיקה לסוגי קלינטים אחרים כדוגמת: בדיקות יחידה, משימות מתוזמנות וmessage queues.
🔗 לקריאה נוספת: חלק את המוצר לשכבות
אמ;לק: מקמו את כל הכלים שאפשר לשתף אותם בתיקייה ייעודית, למשל 'libraries' וכל כלי בתיקייה פנימית נפרדת, למשל '/libraries/logger'. הפכו את הכלי לחבילה בלתי תלויה עם קובץ ה package.json שלו וזאת כדי להגדיל את הכימוס (encapsulation), ואפשרו הפצה עתידית למאגר. כאשר הפרוייקט שלכם בנוי בתצורת monorepo, כלים אלו יכולים להיות מוגדרים על ידי שימוש ב 'npm linking' לכתובת הפיזית שלהם על ידי שימוש ב ts-paths או על ידי הפצה והתקנה על ידימנהל חבילות כדוגמת 'npm registry'.
my-system
├─ apps (components)
│ ├─ component-a
├─ libraries (generic cross-component functionality)
│ ├─ logger
│ │ ├─ package.json
│ │ ├─ src
│ │ │ ├─ index.js
אחרת: צרכנים של כלי יהיו צמודים לפונקציונליות הפנימית שלו. על ידי הגדרה של package.json בשורש הכלי מישהו יכול להגדיר קובץ package.json.main או package.json.exports כדי להצהיר במפורש אילו קבצים ופונקציולניות היא חלק מהחלקים הנגישים של הכלי.
🔗 לקריאה נוספת: בנייה לפי תכונה
אמ;לק: הגדרת סביבה מושלמת צריכה להבטיח כי (א) שמות משתנים יכולים להיקרא מקבצים כמו גם ממשתני סביבה (ב) סודות נשמרים מחוץ לקוד ששייך למאגר (ג) הקונפיגורציה היא היררכית לצורך חיפוש קל יותר (ד) תמיכה בסוגים שונים של משתנים (ה) וידוא מוקדם של משתנים לא תקינים (ו) הגדרת ברירת מחדל לכל שדה. ישנן מספר ספריות שעונות על רוב הדרישות הללו כמו convict, env-var, zod, ועוד...
אחרת: נניח וישנו משתנה סביבה הכרחי שלא הוגדר, המערכת תתחיל לרוץ בהצלחה, תענה לבקשות, חלק מהמידע יעודכן במסד הנתונים, ולפתע יהיה חסר לה שדה הכרחי להמשך התהליך ושבלעדיו היא לא יכולה לסיים את הפעולה, מה שייצור מערכת במצב "מלוכלך".
🔗 לקריאה נוספת: שיטות עבודה של קונפיגורציה
אמ;לק: כאשר בונים אפליקציות ו API-ים, שימוש בפריימוורק הוא חובה. קל להתעלם מהאפשרויות השונות שקיימות ומשיקולים חשובים ובסופו של דבר להשתמש באפשרות שפחות תואמת לדרישות של המוצר. נכון ל2023/2024 אנו מאמינים כי ארבעת הפריימוורקים הללו הם הכדאיים ביותר להשוואה: Nest.js, Fastify, express, ו Koa. לחצו על לקריאה נוספת בהמשך כדי לקרוא פרטים נוספים בעד ונגד כל אחת מהאפשרויות. באופן פשטני, אנו מאמינים כי Node.js זאת ההתאמה הכי טובה לצוותים שרוצים לעבוד בשיטת OOP או לבנות מוצרים שמיועדים לגדול בצורה ניכרת ואי אפשר לחלק אותם לרכיבים קטנים ועצמאיים. ההמלצה שלנו היא Fastify עבור מערכות בגודל סבירents (כמו Microservices) שמושתתים על עקרונות פשוטים של Node.js.
אחרת: בשל הכמות העצומה של השיקולים, קל לקבל החלטה על בסיס מידע חלקי ולהשוות תפוחים לתפוזים. למשל, ישנה הנחה רווחת שFastify הוא web-server מינימלי שראוי להשוות לexpress בלבד. בפועל, זהו פריימוורק עשיר עם הרבה הרחבות רשמיות שמכסות הרבה צרכים.
🔗 לקריאה נוספת: בחירת הפריימוורק הנכון
אמ;לק: קידוד ללא מקדמי בטיחות של סיווג משתנים הוא כבר לא אפשרות בת קיימא, TypeScript מהווה את האפשרות הפופולרית ביותר למשימה זו. משתמשים בה להגדרת סוגי משתנים וערכי החזרה של פונקציות. עם זאת, זוהי חרב פיפיות שיכולה בקלות ליצור מורכבות בשל בסביבות 50 מילות מפתח נוספות שיש לה ותכונות מתוחכמות שצריך לדעת להשתמש בהן. שימוש בה צריך להיעשות במידה, בעדיפות להגדרות פשוטות של משתנים, ושימוש ביכולות מתקדמות רק כאשר צורך הכרחי מופיע.
אחרת: מחקרים מראים כי שימוש ב-TypeScript יכול לעזור בזיהוי כ20% מהבאגים בשלבים מוקדמים יותר. ללא TypeScript חווית הפיתוח ב IDE נהיית בלתי נסבלת. מהצד השני, 80% מהבאגים היא לא עוזרת לזהות. כתוצאה מכך, שימוש בTypeScript מוסיף ערך מוגבל. רק הוספה של בדיקות איכותיות יכולה לעזור לזהות את מגוון הבאגים הרחב, כולל כאלו שנגרמים מאפיון לא תקין של סוג המשתנה. שימוש לא טוב גם עלול להרוג את המטרה, תכונות מורכבות של קוד מעלות אתמורכבות הקוד מה שבאופן ישיר מעלה את מספר הבאגים וזמן התיקון של כל באג.
🔗 לקריאה נוספת: שיקולים לשימוש ב-TypeScript
אמ;לק: ניהול שגיאות אסינכרוניות על ידי שימוש ב-callbacks זו הדרך המהירה לגהינום (הידועה בשם פירמידת דום). המתנה הטובה ביותר שאפשר לתת לקוד הוא שימוש ב-promises בסגנון async-await דבר שמאפשר קוד הרבה יותר נקי ומסודר וסינטקס דומה ל try-catch.
אחרת: סגנון הכתיבה function(err, response)
הכולל שימוש ב-callbacks של Node.js, סולל דרך בטוחה לקוד שאי אפשר לתחזק בשל הערבוב בין ניהול שגיאות לניהול התהליך התקני של המערכת, עם קינון מוגזם וסגנון קוד מוזר.
🔗 לקריאה נוספת: הימנעות מ-callbacks
אמ;לק: ישנן ספריות שזורקות שגיאה כמחרוזת או כאובייקט פרי מחשבת כותבי הקוד של הספריה - דבר שיוצר מורכבות בניהול השגיאות וביצירת מכנה משותף בין מודולים שונים. במקום זאת, השקיעו ביצירת אובייקט או מחלקת (class) שגיאה שיורשת מאובייקט השגיאה המובנה של השפה והשתמשו בזה בכל פעם שצריך לדחות את המצב, לזרוק שגיאה או להפיץ שגיאה. השגיאה האפליקטיבית צריכה להוסיף שדות נוספים כדוגמת שם השגיאה ורמת החומרה שלה. על ידי כך, לכל השגיאות ישנו מבנה אחיד והן מאפשרות תמיכה טובה יותר בניהול שגיאות. ישנו כלל של no-throw-literal
ESLint שבודק בצורה מיטבית את השימוש הזה (על אף שיש לזה קצת מגבלות שיכולות להסתדר על ידי שימוש ב-TypeScript והגדרת החוק @typescript-eslint/no-throw-literal
)
אחרת: כאשר מפעילים רכיב כלשהו, אם ישנה אי וודאות איזה סוג של שגיאה יגיע - זה גורם לכך שניהול השגיאות יהיה הרבה יותר מורכב. גרוע מכך, שימוש באובייקטים מומצאים לתיאור שגיאות עלול להוביל לאיבוד של שגיאות קריטיות בעלות מידע חשוב כמו מעקב אחר מקור השגיאה!
🔗 לקריאה נוספת: שימוש באובייקט השגיאה המובנה
אמ;לק: שגיאות תפעוליות (למשל קלט לא תקין בפנייה ל-API) מתייחסות למקרים ידועים בהם ההשפעה של השגיאה מובנת לחלוטין ויכולה להיות מנוהלת בצורה מחושבת. מצד שני, שגיאות קטסטרופליות (ידועות גם כשגיאות תכנות) מתייחסות לשגיאות לא צפויות במערכת שדורשות אתחול בטוח שלה.
אחרת: אתם עלולים לאתחל את המערכת בעקבות כל שגיאה. אבל למה לגרום לכ-5000 משתמשים לחוות התנתקות בגלל שגיאה תפעולית צפויה ושולית? ההיפך הוא גם לא אידיאלי - להשאיר את המערכת עובדת כאשר קטסטרופה לא צפויה קרתה בה והיא עלולה לגרור התנהגות בלתי צפויה. הבדלה בין שני המקרים מאפשרת התמודדות מושכלת ומאוזנת בהתאם להקשר.
🔗 לקריאה נוספת: שגיאות תפעוליות מול שגיאות תכנות
אמ;לק: מימוש הניהול של השגיאות כמו למשל תעוד השגיאה, החלטה אם לקרוס ואילו מדדים לנטר צריך להיות מרוכז במקום אחד שכל הכניסות למערכת (למשל APIs, cron jobs, scheduled jobs) משתמשות בו כאשר חלה בהן שגיאה.
אחרת: אם לא מנהלים את השגיאות במקום אחד אז במהרה יהיה שכפול קוד וכנראה ניהול לא תקין של חלק מהשגיאות.
🔗 לקריאה נוספת: ניהול השגיאות במקום מרוכז
אמ;לק: אפשרו למשתמשי ה-API שלכם לדעת אילו שגיאות עלולות להגיע כתשובה, כך שהם יוכלו להתמודד איתן בצורה מושכלת במקום לקרוס. ל-API מבוסס REST זה נעשה בדרך כלל באמצעות כלי תעוד כמו OpenAPI. אם אתם משתמשים ב-GraphQL, אתם יכולים להשתמש בסכמה ובהערות בשביל להשיג את המטרה.
אחרת: מי שמשתמש ב-API שלנו עלול להחליט לגרום למערכת שלו לקרוס ולאתחל את עצמה רק בגלל שהוא קיבל שגיאה שהוא לא הצליח להבין. שימו לב: המשתמש של ה-API שלכם יכול להיות אתם (מה שקורה הרבה כשמשתמשים במיקרוסרוויסים).
🔗 לקריאה נוספת: תיעוד שגיאות ה-API באמצעות OpenAPI או GraphQL
אמ;לק: כאשר שגיאה לא ידועה חלה (שגיאה קטסטרופלית, ראו תובנה 2.3) - ישנה חוסר ודאות לגבי הבריאות והיציבות של המערכת. במקרה כזה, אין דרך לברוח מלגרום לשגיאה להיות ברת צפייה, סגירת חיבוריות לרכיבים נוספים והורדה של התהליך. כל סביבת ריצה מהימנה כדוגמת שירותי Docker או שירותי ענן שמספקים פתרונות ללא שרת (serverless) יוודאו שהתהליך יעלה מחדש עבורכם.
אחרת: כאשר שגיאה לא צפויה קורית, רכיב כלשהו עלול להיות במצב לא תקין (למשל event emitter גלובאלי שמפסיק להפיץ אירועים בשל כשלון פנימי) והחל מעכשיו שאר הבקשות שמשתמשות ברכיב זה עלולות להיכשל או להתנהג באופן ממש לא צפוי.
אמ;לק: כלי לוגים איכותי כדוגמת Pino או Winston מגדיל את הקריאות וההבנה של הלוגים על ידי שימוש ברמת חומרה, עימוד, עיצוב, צבעים ועוד. ל-console.log
אין את היכולות הללו וראוי להימנע משימוש בו. העיפרון החד ביותר בתחום מאפשר הוספה של שדות שימושיים נוספים ללא תקורה גבוהה של ביצועים. מפתחים צריכים לכתוב את הלוגים ל-stdout
ולתת לתשתית להעביר את המידע לכלי המתאים עבור כל מקרה.
אחרת: רפרוף על שורות console.log או בצורה ידנית על קבצי טקסט עמוסים לעייפה ללא כלי חיפוש ותצוגה מותאמים עלולים להשאיר אתכם לעבוד עד השעות הקטנות של הלילה.
🔗 לקריאה נוספת: שימוש ב-Logger אמין
אמ;לק: בין אם יש לכם כלי QA אוטומטי ומקצועי ובין אם אחד המפתחים מבצע את הבדיקות - ודאו כי לא רק המסלול הבטוח של הקוד מכוסה, אלא גם ניהול השגיאות ושחוזרות השגיאות שאמורות לחזור במקרה של תקלה. נוסף על כך, בידקו מקרים מורכבים יותר של שגיאות, כמו למשל שגיאות בלתי צפויות, כדי לוודא שהרכיב שמטפל בשגיאות מבצע זאת כראוי (ראו דוגמאות קוד בקישור "לקריאה נוספת")
אחרת: ללא בדיקות כלל, לא ידניות ולא אוטומטיות, לא תוכלו לסמוך על הקוד שלכם שיחזיר את השגיאה הנכונה. ללא שגיאות משמעותיות לא תוכלו לטפל בשגיאות.
🔗 לקריאה נוספת: בדיקת התנהגות בעת שגיאה
אמ;לק: כלי ניטור ובדיקת ביצועים (מוכרים כ-APM) מודדים באופן יזום את הקוד או ה-API כך שבאופן קסום הם מציגים שגיאות, התרסקויות וחלקים שעובדים לאט מהצפוי ואתם לא שמים לב אליהם.
אחרת: אתם עלולים להתאמץ רבות במדידה של בעיות ביצועים וזמני השבתה של המערכת, כנראה שלעולם לא תהיו מודעים לאיזה חלקים במערכת הם האיטיים ביותר ואיך זה משפיע על חווית המשתמש.
אמ;לק: כל שגיאה או דחייה שחוזרת מהבטחה תיבלע, אלא אם כן בשלב הפיתוח יטפלו בה כמו שצריך. אפילו אם יש בקוד האזנה ל process.uncaughtException
! כדי להתגבר על זה צריך להאזין גם ל process.unhandledRejection
.
אחרת: השגיאות במערכת יבלעו ויעלמו ללא עקבות. לא משהו שצריך לדאוג ממנו...
🔗 לקריאה נוספת: תפיסה של דחיות של הבטחות
אמ;לק: הגדירו תבנית קלט קשיחה ל-API כדי להימנע מבאגים מלוכלכים שקשה הרבה יותר לעקוב אחריהם. כתיבת קוד האימות הוא תהליך מייגע, אלא אם כן תשתמשו באחת הספריות המוכרות כיום כמו ajv, zod, או typebox.
אחרת: חשבו על זה - הפונקציה שלכם מצפה לקבל כקלט משתנה discount
מספרי שמי שקרה לפונקציה שכח להעביר. בהמשך, הקוד בודק אם discount != 0
(כמות ההנחה שאפשר לקבל גדולה מאפס), ואם כן אז המשתמש יהנה מההנחה. וואו, זה באג מלוכלך, ראיתם???
אמ;לק: תמיד כתבו return await
כאשר מחזירים תוצאה של הבטחה וזאת כדי להשיג ערך מלא של מעקב אחר מקור השגיאה (stacktrace). אם פונקציה מחזירה הבטחה היא חייבת להיות מוגדרת כפונקציה אסינכרונית ובמפורש לחכות להבטחה שהיא מחזירה.
async function promisifyFunction() {
// some logic
return await new Promise(...);
}
אחרת: הפונקציה שמחזירה הבטחה ללא המתנה לא תופיע בנתיב המעקב אחרי השגיאה (stacktrace). חוסרים כאלו עלולים לסבך את ההבנה של זרימת המערכת שגרמה לשגיאה, במיוחד אם הגורם להתנהגות הלא צפויה קרה בפונקציה החסרה.
אמ;לק: ESLint הוא הסטנדרט דה-פקטו למציאת שגיאות בקוד ותיקון של סגנונות קוד, לא רק זיהוי של רווח סורר שעלול ליצור תקלה אלא גם זיהוי של קוד שלא עומד בסטנדרטים (anti-pattern) כמו זריקת שגיאות ללא סיווג. אמנם ESLint יכול לתקן באופן אוטומטי סגנונות קוד, אך כלים אחרים כדוגמת prettier טובים יותר בעיצוב וסגנון הקוד ועובדים בשילוב עם ESLint.
אחרת: מפתחים ישתעממו תוך כדי השקעת זמנם במציאת רווחים סוררים וידאגו לאורך השורה והזמן היקר שלהם יבוזבז על איך לשמור על סגנון הקוד של הפרוייקט.
🔗 לקריאה נוספת: שימוש ב-ESLint ו-Prettier
אמ;לק: על גבי הסטנדרט של חוקי ESLint שמכסים את שפת JavaScript, הוסיפו את התוספים היעודיים של Node.js כמו eslint-plugin-node, eslint-plugin-mocha, eslint-plugin-node-security, eslint-plugin-require, eslint-plugin-jest ועוד תוספים שמממשים חוקים נוספים ומועילים.
אחרת: הרבה תבניות קוד לא תקינות שבשימוש ב-Node.js נעלמות מתחת לרדאד. לדוגמה, מפתחים יכתבו require(variableAsPath)
עם משתנה שמאפשר גישה לתיקיה בקוד, דבר שמאפשר לתוקפים להריץ כל קוד JS. אם תשתמשו בחוקי Node.js תוכלו לזהות את הטעות הזאת ולקבל עליה התראה מבעוד מועד.
אמ;לק: מומלץ שהסוגריים המסולסלים הפותחים של בלוק של קוד יהיו באותה השורה יחד עם הקוד.
// Do
function someFunction() {
// code block
}
// Avoid
function someFunction()
{
// code block
}
אחרת: התעלמות משיטת עבודה זו עלולה להוביל לתוצאות לא צפויות, כמו שניתן לראות בשרשור בקישור מ StackOverflow:
🔗 לקריאה נוספת: "למה התוצאות משתנות בהתאם למיקום הסוגר המסולסל?" (StackOverflow)
בין אם אתם משתמשים בנקודה-פסיק (;) בשביל להפריד בין ההצהרות על המשתנים ובין אם לא, עצם הידיעה על ההשלכות של ירידת שורה במקום הלא מתאים או של הוספה אוטומטית של נקודה-פסיק, יעזרו לכם לזהות שגיאות סינטקס רגילות.
אמ;לק: שימוש ב-ESLint כדי להעלות את המודעות לגבי הסיכון הכרוך בזה. כלים כמו Prettier או Standardjs יכולים באופן אוטומטי לפתור את הבעיות הללו.
אחרת: כמו שראינו בסעיף הקודם, "המתורגמן" (interpreter) של JavaScript מוסיף אוטומטית נקודה-פסיק בסוף כל הצהרה במידה ואין, או שהוא מחליט כי ההצהרה מסתיימת במקום אחר מהמתוכנן על ידינו, דבר שעלול להוביל לתוצאות בלתי צפויות. אפשר להשתמש בהשמות ולהימנע מ IIFE כדי להימנע מרוב ההתנהגויות הבלתי צפויות.
// Do
function doThing() {
// ...
}
doThing()
// Do
const items = [1, 2, 3]
items.forEach(console.log)
// Avoid — throws exception
const m = new Map()
const a = [1,2,3]
[...m.values()].forEach(console.log)
> [...m.values()].forEach(console.log)
> ^^^
> SyntaxError: Unexpected token ...
// Avoid — throws exception
const count = 2 // it tries to run 2(), but 2 is not a function
(function doSomething() {
// do something amazing
}())
// put a semicolon before the immediate invoked function, after the const definition, save the return value of the anonymous function to a variable or avoid IIFEs altogether
🔗 לקריאה נוספת: "Semi ESLint rule"
🔗 לקריאה נוספת: "No unexpected multiline ESLint rule"
אמ;לק: תנו שמות לכל הפונקציות, כולל closures ו-callbacks. הימנעו מפונקציות אנונימיות. זה מאוד שימושי כשבודקים אפליקציות Node.js. מתן שמות לכל הפונקציות יאפשר לכם להבין בקלות על מה אתם מסתכלים כשאתם צופים בתמונת מצב של הזיכרון של האפליקציה.
אחרת: לדבג את גרסת היצור (production) על בסיס תמונת מצב של הזיכרון (core dump) עלול להיות מאתגר כשהבעיות של הזיכרון קורות בכל מיני פונקציות אנונימיות.
אמ;לק: השתמשו ב-lowerCamelCase כאשר אתם נותנים שמות לקבועים, משתנים ופונקציות, UpperCamelCase (גם האות הראשונה גדולה) כאשר אתם נותנים שמות למחלקות ו-UPPER_SNAKE_CASE כאשר אתם נותנים שמות למשתנים גלובליים או סטטיים. סדר זה יאפשר לכם להבחין בקלות בין משתנים רגילים ופונקציות לבין מחלקות שדורשות אתחול ולבין משתנים גלובליים. השתמשו בשמות שמתארים היטב את משמעות המשתנה, אך שיהיה קצר.
אחרת: JavaScript היא השפה היחידה בעולם שתאפשר לכם לקרוא ל-constructor ("Class") ישירות ללא אתחול. לכן, חשוב מאוד להבדיל בין שמות מחלקות ושמות פונקציות על ידי שימוש ב-UpperCamelCase.
// for global variables names we use the const/let keyword and UPPER_SNAKE_CASE
let MUTABLE_GLOBAL = "mutable value";
const GLOBAL_CONSTANT = "immutable value";
const CONFIG = {
key: "value",
};
// examples of UPPER_SNAKE_CASE convention in nodejs/javascript ecosystem
// in javascript Math.PI module
const PI = 3.141592653589793;
// https://github.com/nodejs/node/blob/b9f36062d7b5c5039498e98d2f2c180dca2a7065/lib/internal/http2/core.js#L303
// in nodejs http2 module
const HTTP_STATUS_OK = 200;
const HTTP_STATUS_CREATED = 201;
// for class name we use UpperCamelCase
class SomeClassExample {
// for static class properties we use UPPER_SNAKE_CASE
static STATIC_PROPERTY = "value";
}
// for functions names we use lowerCamelCase
function doSomething() {
// for scoped variable names we use the const/let keyword and lowerCamelCase
const someConstExample = "immutable value";
let someMutableExample = "mutable value";
}
אמ;לק: שימוש ב-const
משמעותו היא שלאחר שהמשתנה מאותחל לראשונה הוא לא יכול להיות מאותחל שוב. העדפת שימוש ב-const
תעזור לכם לא להתפתות ולהשתמש שוב באותו משתנה לצרכים שונים ותהפוך את הקוד שלכם לקריא יותר. אם משתנה צריך להיות מאותחל מחדש, למשל בתוך לולאת for, אז השתמשו ב-let
לצורך כך. נקודה נוספת שחשוב לציין היא ששימוש ב-let
אפשרית רק בתוך אותו הבלוק שהיא הוגדרה בו. var
נצמד לscope של הפונקציה שהוא מוגדר בו ולא לבלוק ספציפי ולכן צריך לא להשתמש בו ב-ES6 כשאפשר להשתמש ב-const
וב-let
.
אחרת: דיבוג הופך להיות מאוד מסורבל כאשר משתנה משתנה לעיתים דחופות.
🔗 לקריאה נוספת: JavaScript ES6+: var, let, or const?
אמ;לק: טענו את המודולים (require...) בתחילת כל קובץ, לפני כל הפונקציות. שיטת עבודה פשוטה זו לא רק שתעזור לכם בקלות ובמהירות לזהות את התלויות של קובץ מסוים, אלא גם תמנע מספר בעיות אפשריות.
אחרת: טעינת מודולים היא תהליך סינכרוני ב-Node.js. אם הטעינה תתבצע מתוך פונקציה היא עלולה לחסום טיפול בבקשות אחרות בזמן קריטי. בנוסף לכך, אם מודול חיוני או מישהו שהוא תלוי בו יזרקו שגיאה ויפילו את השרת, מומלץ שזה יוודע כמה שיותר מוקדם, מה שלא בטוח יקרה במקרה שהמודול נטען מתוך פונקציה.
אמ;לק: בעת פיתוח מודול או ספריה, הגדירו קובץ בסיס שמייצא את הקוד המיועד לשימוש חיצוני. מנעו מהמשתמשים של הקוד שלכם את הצורך לייבא קבצים שיושבים עמוק אצלכם ואת הצורך שלהם להבין את מבנה הקבצים שלכם. כאשר עובדים בשיטת commonjs (require), זה יכול להיעשות על ידי שימוש בקובץ index.js שיושב בתיקיה הראשית או בהגדרת השדה main בקובץ package.json. כאשר עובדים בשיטת ESM (import), אם קובץ package.json קיים בתיקיה הראשית, אז השדה "exports" מאפשר את הגדרת הקובץ הראשי. אך אם אין קובץ package.json, אז שימוש בקובץ index.js בתיקיה הראשית ייצא את כל הפונקציונליות שמיועדת לשימוש חיצוני.
אחרת: קיומו של קובץ ראשי רשמי משמש כממשק חיצוני שמסתיר את החלקים הפנימיים של הספריה, מקשר את המשתמש ישירות לקוד הזמין ומאפשר שינויים עתידיים ללא צורך לשבוראת החוזה.
// Avoid: client has deep familiarity with the internals
// Client code
const SMSWithMedia = require("./SMSProvider/providers/media/media-provider.js");
// Better: explicitly export the public functions
//index.js, module code
module.exports.SMSWithMedia = require("./SMSProvider/providers/media/media-provider.js");
// Client code
const { SMSWithMedia } = require("./SMSProvider");
אמ;לק: העדיפו את ההשוואה הקפדנית באמצעות האופרטור ===
על פני ההשוואה החלשה יותר באמצעות האופרטור ==
. ==
משווה שני משתנים אחרי המרה של שניהם לסוג משתנה אחד. אין המרת סוגי משתנים באופרטור ===
, ושני המשתנים חייבים להיות מאותו סוג כדי שיוכלו להיות שווים.
אחרת: משתנים בעלי ערכים שונים עלולים להחזיר true
כאשר משווים ביניהם בעזרת האופרטור ==
.
"" == "0"; // false
0 == ""; // true
0 == "0"; // true
false == "false"; // false
false == "0"; // true
false == undefined; // false
false == null; // false
null == undefined; // true
" \t\r\n " == 0; // true
כל ההשוואות לעיל יחזירו false
בעת השוואה עם ===
.
אמ;לק: async-await זו הדרך הפשוטה ביותר לכתוב קוד אסינכרוני שירגיש כמו קוד סינכרוני. הקוד שיכתב בשיטת async-await הוא גם הרבה יותר פשוט ותומך במנגנון ה-try-catch. שיטה זו מחליפה את הצורך ב-callbacks ו-promises ברוב המקרים. שימוש בשיטה זו בקוד היא כנראה אחת המתנות הטובות יותר שאפשר לתת למי שיקרא את הקוד.
אחרת: טיפול בשגיאות אסינכרוניות בשיטת callback היא כנראה הדרך המהירה לגהנום - מכיוון ששיטה זו מחייבת בדיקת שגיאות בכל שלב, יוצרת קינון מוזר בקוד ומקשה על הבנת תהליך הזרימה של הקוד.
🔗לקריאה נוספת: מדריך ל-async-await
אמ;לק: אמנם מומלץ להשתמש ב async-await ולהימנע מהגדרת פרמטרים בפונקציות כאשר מתעסקים עם API ישן שתומך ב-callbacks או הבטחות - פונקציות חץ מאפשרות לארגן את הקוד קומפקטי יותר וכמובן ששומרות על הקונטקסט של פונקצית המעטפת (this
).
אחרת: קוד ארוך יותר (על בסיס פונקציות של ES5) חשוף ליותר באגים וקשה יותר לקריאה.
🔗 לקריאה נוספת: הגיע הזמן לאמץ את פונקציות החץ
אמ;לק: הימנעו מכתיבת קוד עם השפעות צדדיות כמו פעולת רשת או פניה למסד נתונים מחוץ לפונקציה. אם כן תכתבו קוד כזה הוא ירוץ מיד כאשר קובץ אחר פונה לקובץ הזה. הקוד 'הצף' הזה עלול לרוץ כאשר התשתית אותה הוא מבקש עוד לא זמינה עבורו. זה גם פוגע בביצועים אפילו אם אין צורך בפונקציה שעבורה מתבצעת הפעולה בזמן הריצה. דבר אחרון, כתיבת כיסוי לפעולה זו בשביל בדיקות הרבה יותר מורכבת כשהיא לא נעשית בפונקציה. במקום זאת, שימו את הקוד הזה בפונקציה שצריכה להיקרא במפורש. אם הקוד הזה צריך להיקרא ישר בעת עליית המערכת, שיקלו שימוש ב-factory או בתבנית אחרת שמתאימה לדרישה כזאת.
אחרת: תשתיות סטנדרטיות בעולם הווב מגדירות ניהול שגיאות, משתני סביבה וניטור תקלות. אם הפעולה תתבצע לפני שהתשתית מאותחלת אז לא יהיה ניטור של המקרה או שהפעולה תיכשל בשל חוסר בהגדרות שטרם נטענו.
יש לנו מדריכים יעודיים לכתיבת בדיקות. רשימת שיטות העבודה המומלצות פה היא סיכום כללי של המדריכים הללו.
א. שיטות עבודה מומלצות בכתיבת בדיקות ל-JavaScript
ב. בדיקות ב-Node.js - מעבר ליסודות
אמ;לק: ברוב הפרויקטים אין בדיקות אוטומטיות כלל בשל לוח זמנים קצר, או שהתחילו לנסות להוסיף בדיקות בפרויקט נוסף אך זה יצא משליטה וננטש עם הזמן. לכן, לתעדף ולהתחיל בדיקות API שזאת הדרך הקלה לכתוב בדיקות ולספק כיסוי (בדיקות) של הקוד מאשר בבדיקות יחידה של פונקציות בודדות (אפשר להשתמש בשביל זה גם בכלים חיצוניים ללא כתיבת קוד, למשל שימוש ב-Postman). לאחר מכן, אם יש לכם יותר משאבים וזמן תמשיכו עם בדיקות מתקדמות יותר כגון בדיקות יחידה, בדיקות מול מסדי הנתונים בדיקות ביצועים ועוד.
אחרת: אתם עלולים לבזבז ימים שלמים על כתיבת בדיקות יחידה בלבד ולגלות בסופו של דבר שכיסיתם רק 20% מהמערכת.
אמ;לק: גירמו לבדיקה לתאר את שלב הדרישות כך שהיא תסביר את עצמה גם לQA או לאחרים (כולל אתכם בעתיד הלא רחוק) שלא בקיאים בחלקים הפנימיים של הקוד. ציינו בבדיקה (1) איזה חלק נבדק, (2) באילו תנאים (3) ומה התוצאה שמצפים שתחול.
אחרת: ההתקנה בדיוק נכשלה, בדיקה בשם “Add product” נכשלה. האם זה מתאר מה בדיוק לא תיפקד?
🔗 לקריאה נוספת: סווגו 3 חלקים במתן שם לכל בדיקה
אמ;לק: חלקו את הבדיקות לשלושה חלקים נפרדים: Arrange (ארגן), Act (פעל) & Assert (ודא) (AAA). החלק הראשון כולל את ההכנה של הסביבה לבדיקה, החלק השני את ההרצה במצב בדיקות, ולבסוף החלק שמוודא שהתקבלה התוצאה הרצויה. שימוש במבנה זה בעקביות מבטיח שהקורא לא יבזבז זמן מחשבה של הבנת הבדיקה.
אחרת: לא מספיק שיתבזבז זמן נרחב מהיום על הבנת הקוד, עכשיו גם החלק הקל ביום (הבנת הבדיקות) ישרוף את המוח.
🔗 לקריאה נוספת: חלקו את הבדיקות לפי תבנית ה-AAA
אמ;לק: השתמשו בכלים המעודדים או אוכפים שימוש באותה גרסת Node.js בסביבות השונות ועל ידי שאר המפתחים. כלים כמו nvm, ו-Volta מאפשרים להגדיר במפורש את הגרסה הנדרשת בפרויקט בקובץ כך שכל חברי הצוות יכולים על ידי הרצת פקודה אחת ליישר קו עם גרסת הפרויקט. ישנה אפשרות שגרסה זו גם תשתקף לתהליך ה-CI וסביבת היצור/לקוחות (לדוגמה על ידי העתקת מספר הגרסה המבוקש ל-.Dockerfile
ולקבצי ההגדרות של תהליך ה-CI).
אחרת: מפתחת עלולה להיתקל או לפספס שגיאה מכיוון שהיא משתמשת בגרסת Node.js שונה משאר הצוות. או גרוע מכך, סביבת היצור רצה באמצעות גרסה שונה מזו שהורצו עליה הבדיקות.
אמ;לק: כדי להימנע מצמידות ותלות בין בדיקות שונות וכדי שיהיה ברור יותר איך להסביר מה קורה בשלבים השונים של הבדיקה, ראוי שכל בדיקה תוסיף ותנהל את המידע העוטף שלה (למשל שורות בטבלה). במקרה ובדיקה צריכה לצרוך מידע מטבלה או להניח שהוא קיים שם - היא צריכה קודם לכן להוסיף את המידע במפורש ולהימנע משינוי מידע של בדיקה אחרת.
אחרת: תארו לכם מקרה בו הפצת גרסה נכשלה בשל שגיאה בבדיקות, הצוות משנס מותניים לחקור את הסיבה ומגיע אם התובנה העצובה שהמערכת עובדת תקין אבל הבדיקות דורסות מידע אחת לשניה ולכן נכשלו ועצרו את תהליך ההפצה.
🔗 לקריאה נוספת: הימנעו מאתחול מידע גרעיני משותף
אמ;לק: בדיקות שונות צריכות לרוץ בתרחישים שונים: בדיקות שפיות (quick smoke/sanity), IO-less, בדיקות בעת שמירת קובץ או commit, בדיקות מלאות מקצה לקצה (e2e) כאשר נפתח PR וכולי... התרחישים השונים יכולים להיות מוגדרים בעזרת תיוג בדיקות שונות עם מילות מפתח כמו #cold #api #sanity דבר המאפשר להגדיר קבוצת בדיקות בהתאם לצורך ולהריץ רק אותה. למשל, זאת השיטה להריץ רק את קבוצת בדיקות השפיות באמצעות Mocha: mocha --grep 'sanity'
.
אחרת: הרצה של כל הבדיקות כולל כאלו שמבצעות עשרות פניות למסד נתונים בכל פעם שמפתח עושה שינוי קטן יאט את קצב הפיתוח בצורה ניכרת ותמנע מצוות הפיתוח להריץ בדיקות.
אמ;לק: כלים לבדיקת כיסוי הקוד על ידי בדיקות כמו Istanbul/NYC מצוינים בשל שלוש סיבות: הם בחינם (אין עלות לדו"חות שהם מספקים), הם עוזרים לזהות ירידה באחוזי הכיסוי, ואחרון חביב הם מדגישים מקרים של אי התאמה בבדיקות: על ידי צפייה בצבעים שהדוחות הללו מספקים אפשר לזהות למשל שיש קטעי קוד שלא נבדקים לעולם כמו הסתעפויות של catch
(מה שאומר שיש בדיקות רק למסלול המצליח ולא למקרים של השגיאות). רצוי להגדיר את זה כך שזה יפיל את תהליכי יצירת הגרסאות במידה והכיסוי לא עובר סף מסוים.
אחרת: לא יהיה שום אמצעי מדידה שידווח שקטעים נרחבים מהקוד לא נבדקים כלל.
אמ;לק: End to end (e2e) testing which includes live data used to be the weakest link of the CI process as it depends on multiple heavy services like DB. Use an environment which is as close to your real production environment as possible like a-continue (Missed -continue here, needs content. Judging by the Otherwise clause, this should mention docker-compose)
אחרת: Without docker-compose, teams must maintain a testing DB for each testing environment including developers' machines, keep all those DBs in sync so test results won't vary across environments
אמ;לק: שימוש בכלי ניתוח סטטי (static analysis tools) עוזר בכך שהוא נותן דרכים מתאימות לשפר את איכות הקוד ולשמור על הקוד מתוחזק. אפשר להוסיף כלים כאלו לשלבי הבנייה ב-CI כך שיפילו את התהליך במידה והם מזהים ניחוחות בקוד. אחד היתרונות העיקריים שלהם על פני כלים פשוטים יותר הוא היכולת לזהות פגמים באיכות הקוד על פני מספר קבצים (כמו כפל קוד), מורכבות גבוהה של קוד ומעקב אחרי ההיסטוריה וההתקדמות של הקוד. שני כלים מומלצים לשימוש הם Sonarqube (7,900+ stars) ו Code Climate (2,400+ stars).
אחרת: אם הקוד באיכות נמוכה, תקלות ובעיות ביצועים תמיד יהוו אתגר שאף ספריה חדשה ונוצצת או פתרון טכנולוגי חדיש יוכלו לפתור.
אמ;לק: השתמשו בכלי הדמיה של המידע שמגיע מהרשת עבור תשובות שמגיעות משירותים חיצוניים (כמו בקשות REST ו GraphQL). זה הכרחי לא רק כדי לבודד את הרכיב שנבדק אלא בעיקר כדי לבדוק מצבים לא צפויים. כלים כמו nock או Mock-Server מאפשרים להגדיר תשובה מסוימת לבקשה לשירות חיצוני בשורת קוד בודדה. חשוב לא לשכוח לדמות גם שגיאות, עיכובים, timeouts, וכל אירוע אחר שכנראה יקרה בסביבת הייצור.
אחרת: לאפשר לרכיב לגשת למידע אמיתי משירותים חיצוניים בדרך כלל יסתיים בבדיקות פשוטות שמכסות בעיקר את המקרים שהכל טוב. בנוסף לכך הבדיקות לפעמים יכשלו ויהיו איטיות יותר.
🔗 לקריאה נוספת: הדמיית שירותים חיצוניים
אמ;לק: כאשר פונקציית ביניים (middleware) אוחזת נתח משמעותי של לוגיקה שמשתרעת על פני מספר עצום של בקשות, כדאי לבדוק אותה בצורה מבודדת ללא צורך לטעון את כל תשתית הפריימוורק. אפשר להשיג את הפעולה הזאת בקלות על ידי עטיפה או הדמיה של {req, res, next}
.
אחרת: באג בפונקציות ביניים ב-express
=== באג ברוב הקריטי של הבקשות.
🔗 לקריאה נוספת: לבדוק פונקציות ביניים בנפרד
אמ;לק: כאשר מבצעים בדיקות מול API, זה רצוי ואף נהוג לאתחל את השרת בתוך הבדיקות. תנו לשרת לבחור פורט באופן אקראי כאשר מריצים בדיקות כדי למנוע התנגשויות. אם אתם משתמשים בשרת HTTP של Node.js (בשימוש על ידי רוב ספריות התשתית), כדי להשיג את היכולת הזאת אין צורך לעשות כלום מלבד להעביר port=0 - זה כבר יגרום להקצאה דינאמית של פורט.
אחרת: הגדרה של פורט ספציפי ימנע את האפשרות להריץ שני טסטים במקביל. רוב הכלים שמריצים כיום טסטים - מריצים במקביל כברירת מחדל.
🔗 לקריאה נוספת: הגדירו פורט אקראי לבדיקות
אמ;לק: בעת בדיקת מקרה, ודאו שאתם מכסים את חמשת הקטגוריות האפשריות. בכל פעם שפעולה חלה (למשל קריאת API), מתחילה תגובה, תוצאה משמעותית נוצרת ומתבצעת קריאה לבדיקה. ישנן חמש סוגי תוצאות לכל מקרה: תגובה, שינוי נראה לעין (כמו עדכון במסד הנתונים), שליחת קריאה ל- API, הודעה חדשה נרשמת לתור, וקריאה לכלי צפיה במידע (כמו לוגר ואנליטיקות). רשימת בדיקות בסיסיות. כל סוג של תוצאה מגיע אם אתגרים יחודיים ושיטות להמתיק את האתגרים הללו - כתבנו מדריך יעודי על נושא זה בדיקות ב-Node.js - מעבר ליסודות
אחרת: תארו לעצמכם מקרה של בדיקת הוספה של מוצר חדש למערכת. נפוץ לראות בדיקות שמכסות אך ורק את המקרים של תשובה תקינה. מה יקרה אם המוצר לא יתווסף על אף התשובה החיובית? מה צריך להיעשות במידה ובעת הוספת מוצר יש גם קריאה לשירות חיצוני או הוספת הודעה לתור - האם הבדיקה לא צריכה להתייחס גם לזה? קל להתעלם ממגוון מקרים, ובנקודה זאת רשימת הבדיקות עוזרת.
🔗 לקריאה נוספת: בדיקת חמשת התוצאות
אמ;לק: ניטור הוא משחק של מציאת בעיות לפני שהמשתמשים מוצאים אותן - מובן מאליו שזה צריך להיות בראש סדר העדיפויות. השוק מוצף בהצעות להגדרות מה הם המדדים הבסיסיים שחייבים לעקוב אחריהם (ההמלצות שלנו בהמשך), לאחר מכן לעבור על כל היכולות המעניינות שכל מוצר מציע ולבחור את הפתרון המיטבי עבור הדרישות שלכם. בכל מקרה, ארבעת השכבות הניתנות לצפייה חייבות להימדד: (1) Uptime - מציינת האם המערכת זמינה, (2) Metrics - מציינת מהי ההתנהגות המצטברת של המערכת (האם 99% מהבקשות נענות), (3) Logging - בודקת אם בקשה מסויימת מסתיימת בהצלחה, (4) Distributed tracing - בודקת האם המערכת יציבה בין הרכיבים המבוזרים שלה.
אחרת: כשלון === לקוחות מאוכזבים. פשוט מאוד.
אמ;לק: לוגים יכולים להיות פח הזבל של שלל מצבים שהמפתחים רצו לדבג או לחלופין מסך מהמם שמתאר את המצב של המוצר. תכננו את הלוגים שלכם מהיום הראשון: איך הם נאספים, איפה הם נשמרים ואיך הם מנותחים כדי להבטיח שהמידע ההכרחי (אחוז שגיאות, מעקב אחר פעולה בין מספר שירותים וכו') באמת נגיש ובר שימוש.
אחרת: יש לכם קופסה שחורה שקשה להבין למה היא מגיעה למצב הנוכחי, ורק עכשיו אתם מתחילים לשכתב את כל הלוגים שלכם כדי שיהיה מידע רלוונטי.
🔗 לקריאה נוספת: הגדלת השקיפות על ידי לוגים איכותיים
אמ;לק: Node.js גרוע בלבצע פעולות שדורשות עוצמת חישוב גבוהה מה-CPU, כמו למשל דחיסה, סיום תהליך SSL, וכו'... כדאי שתשתמשו בתשתיות כמו nginx, HAproxy או שירותי ענן אחרים לשם כך.
אחרת: הת'רד הבודד והמסכן שלכם יישאר עסוק במשימות תשתיתיות במקום להתעסק בלב המערכת שלכם והביצועים יישחקו בהתאם.
🔗 לקריאה נוספת: האצלת כל מה שאפשר (לדוגמה gzip, SSL) לשירות נפרד
אמ;לק: הקוד שלכם צריך להיות זהה בכל הסביבות, אך ללא קובץ יעודי npm יאפשר שימוש בתלויות שונות בכל סביבה. ודאו כי יש לכם package-lock.json
כך שכל הסביבות יהיו זהות.
אחרת: אנשי הבדיקות יאשרו גרסה שתתנהג אחרת בסביבת ייצור. גרוע מכך, שרתים שונים באותה סביבה יריצו קוד שונה.
אמ;לק: המערכת צריכה להמשיך לעבוד ולהתאתחל במידה וקרתה שגיאה קריטית. סביבות ריצה חדשות כמו למשל כאלו המבוססות דוקר (כמו קוברנטיס), או Serverless מטפלות בזה בצורה אוטומטית. כאשר המוצר מותקן על שרת אמיתי פיזי, יש צורך לנהל את משאבי המערכת בעזרת כלי כמו systemd. אך יש להימנע מלעשות זאת כאשר משתמשים בתשתיות שכבר מבצעות את הניטור מכיוון שזה יגרום לבליעת שגיאות. כאשר לתשתית אין מודעות לשגיאות, אין לה יכולת של ביצוע שלבי פיחות משאבים כמו העברת האינסטנס של המערכת למקום אחר ברשת.
אחרת: הרצה של עשרות אינסטנסים ללא סיבה ברורה ויותר מידי כלי תשתית יחד (cluster management, docker, PM2) עלול לגרום לכאוס עבור ה-DevOps.
🔗 לקריאה נוספת: הבטיחו את זמינות המערכת בעזרת הכלי המתאים
אמ;לק: בתצורה הבסיסית שלה, מערכת מבוססת Node.js תרוץ על מעבד CPU אחד ושאר המעבדים ינוחו. מחובתכם לשכפל את התהליך ולנהל את המערכת ככה שתרוץ על כל המעבדים. רוב תשתיות הריצה החדשות (כמו קוברנטיס) מאפשרות לשכפל את התהליכים למספר מעבדים, אך הן לא מבטיחות להשתמש בכל המעבדים - זאת האחריות שלכם! אם המוצר מותקן על שרת פיזי, אז כחלק מאחריותכם אתם צריכים גם להשתמש בפתרונות שיבצעו את השכפול של התהליך (כמו systemd).
אחרת: המוצר שלכם ינצל לכל היותר 25% מהמשאבים הזמינים(!). זכרו שלשרת רגיל יש 4 מעבדי CPU או יותר, והתקנה סטנדרטית של תהליך Node.js משתמשת רק במעבד אחד (גם שירותים בשיטת PaaS כמו AWS beanstalk!).
🔗 לקריאה נוספת: השתמשו בכל מעבדי ה-CPU
אמ;לק: חישפו מידע רלוונטי על המערכת, למשל מצב הזיכרון ו -REPL, באמצעות API מאובטח. על אף שמומלץ להישען על כלים יעודיים לשם כך, את חלק מהמידע והפעולות יותר פשוט לבדוק באמצעות כתיבת קוד.
אחרת: תגלו שאתם מבצעים הרבה “diagnostic deploys” – העלאת קוד לסביבת הייצור רק כדי להשיג עוד קצת מידע אבחנתי על המערכת.
🔗 לקריאה נוספת: יצירת ‘maintenance endpoint’
אמ;לק: שיקלו הוספת שכבה נוספת של בטיחות למוצר שלכם - APM (Application monitoring and performance products). אמנם רוב הסממנים והגורמים יכולים להימצא על ידי טכניקות ניטור סטנדרטיות, אך במערכות מבוזרות יש עוד רבדים סמויים מן העין. ניטור מערכות ובדיקת ביצועים (או בקיצור APM) יכולים באופן קסום להוסיף שכבה נוספת של חוויית פיתוח מעבר למה שמספקים הכלים הסטנדרטיים. לדוגמה, ישנם כלי APM שיכולים להדגיש טרנזקציה שטוענת לאט מידי את צד הלקוח ולהציע מה הסיבה לכך. כלים אלו גם מספקים יותר הקשר לצוות הפיתוח שמנסים לחקור שגיאה וזאת על ידי הצגה של העומסים שהיו בשרת בזמן שחלה השגיאה.
אחרת: אתם משקיעים זמן ניכר במדידת ביצועי API ואי זמינות של המערכת, כנראה שלעולם לא תהיו מודעים לאילו חלקים בקוד הם האיטיים ביותר בזמן אמת ואיך זה משפיע על חווית המשתמש.
🔗 לקריאה נוספת: גילוי שגיאות וזמני השבתה בעזרת מוצרי APM
אמ;לק: קודדו כאשר התוצאה הסופית במחשבותיכם, התכוננו להתקנה בסביבת יצור כבר מהיום הראשון. זה אמנם נשמע קצת מעורפל ולכן בקישור ישנן מספר המלצות הקשורות לתמיכה במוצר שכבר הותקן.
אחרת: אלופי העולם של IT/DevOps לא ינסו להציל מערכת שכתובה גרוע.
🔗 לקריאה נוספת: כתבו את הקוד מותאם להתקנה
אמ;לק: ל-Node.js ישנה מערכת יחסים מורכבת עם ניהול הזיכרון: למנוע ה-v8 ישנם גבולות עדינים של צריכת זיכרון (1.4GB) וישנן דרכים ידועות איך לגרום לזליגת זיכרון בקוד של Node.js - ולכן מעקב אחר צריכת הזיכרון של תהליך Node.js הוא חובה. במוצרים קטנים, אפשר לאמוד את צריכת הזיכרון כל כמה זמן בעזרת פקודות shell, אבל במוצרים בינוניים-גדולים צריך לתעדף שימוש בכלים חזקים לניטור מצב הזיכרון.
אחרת: זולגים לכם מאות MB כל יום מהתהליך כמו שקרה בוולמארט
🔗 לקריאה נוספת: מדידה ושמירה על ניצול הזיכרון
אמ;לק: Serve frontend content using a specialized infrastructure (nginx, S3, CDN) because Node performance gets hurt when dealing with many static files due to its single-threaded model. One exception to this guideline is when doing server-side rendering
אחרת: Your single Node thread will be busy streaming hundreds of html/images/angular/react files instead of allocating all its resources for the task it was born for – serving dynamic content
🔗 Read More: Get your frontend assets out of Node
אמ;לק: Store any type of data (e.g. user sessions, cache, uploaded files) within external data stores. When the app holds data in-process this adds additional layer of maintenance complexity like routing users to the same instance and higher cost of restarting a process. To enforce and encourage a stateless approach, most modern runtime platforms allows 'reapp-ing' instances periodically
אחרת: Failure at a given server will result in application downtime instead of just killing a faulty machine. Moreover, scaling-out elasticity will get more challenging due to the reliance on a specific server
🔗 Read More: Be stateless, kill your Servers almost every day
אמ;לק: Even the most reputable dependencies such as Express have known vulnerabilities (from time to time) that can put a system at risk. This can be easily be tamed using community and commercial tools that constantly check for vulnerabilities and warn (locally or at GitHub), some can even patch them immediately
אחרת: Keeping your code clean from vulnerabilities without dedicated tools will require you to constantly follow online publications about new threats. Quite tedious
🔗 Read More: Use tools that automatically detect vulnerabilities
אמ;לק: Assign the same identifier, transaction-id: uuid(), to each log entry within a single request (also known as correlation-id/tracing-id/request-context). Then when inspecting errors in logs, easily conclude what happened before and after. Node has a built-in mechanism, AsyncLocalStorage, for keeping the same context across asynchronous calls. see code examples inside
אחרת: Looking at a production error log without the context – what happened before – makes it much harder and slower to reason about the issue
🔗 Read More: Assign ‘TransactionId’ to each log statement
אמ;לק: Set the environment variable NODE_ENV
to ‘production’ or ‘development’ to flag whether production optimizations should get activated – some npm packages determine the current environment and optimize their code for production
אחרת: Omitting this simple property might greatly degrade performance when dealing with some specific libraries like Express server-side rendering
🔗 Read More: Set NODE_ENV=production
אמ;לק: Research shows that teams who perform many deployments lower the probability of severe production issues. Fast and automated deployments that don’t require risky manual steps and service downtime significantly improve the deployment process. You should probably achieve this using Docker combined with CI tools as they became the industry standard for streamlined deployment
אחרת: Long deployments -> production downtime & human-related error -> team unconfident in making deployment -> fewer deployments and features
אמ;לק: Ensure you are using an LTS version of Node.js to receive critical bug fixes, security updates and performance improvements
אחרת: Newly discovered bugs or vulnerabilities could be used to exploit an application running in production, and your application may become unsupported by various modules and harder to maintain
🔗 Read More: Use an LTS release of Node.js
אמ;לק: Log destinations should not be hard-coded by developers within the application code, but instead should be defined by the execution environment the application runs in. Developers should write logs to stdout
using a logger utility and then let the execution environment (container, server, etc.) pipe the stdout
stream to the appropriate destination (i.e. Splunk, Graylog, ElasticSearch, etc.).
אחרת: If developers set the log routing, less flexibility is left for the ops professional who wishes to customize it. Beyond this, if the app tries to log directly to a remote location (e.g., Elastic Search), in case of panic or crash - further logs that might explain the problem won't arrive
אמ;לק: Run npm ci
to strictly do a clean install of your dependencies matching package.json and package-lock.json. Obviously production code must use the exact version of the packages that were used for testing. While package-lock.json file sets strict version for dependencies, in case of mismatch with the file package.json, the command 'npm install' will treat package.json as the source of truth. On the other hands, the command 'npm ci' will exit with error in case of mismatch between these files
אחרת: QA will thoroughly test the code and approve a version that will behave differently in production. Even worse, different servers in the same production cluster might run different code.
אמ;לק: Make use of security-related linter plugins such as eslint-plugin-security to catch security vulnerabilities and issues as early as possible, preferably while they're being coded. This can help catching security weaknesses like using eval, invoking a child process or importing a module with a string literal (e.g. user input). Click 'Read more' below to see code examples that will get caught by a security linter
אחרת: What could have been a straightforward security weakness during development becomes a major issue in production. Also, the project may not follow consistent code security practices, leading to vulnerabilities being introduced, or sensitive secrets committed into remote repositories
אמ;לק: DOS attacks are very popular and relatively easy to conduct. Implement rate limiting using an external service such as cloud load balancers, cloud firewalls, nginx, rate-limiter-flexible package, or (for smaller and less critical apps) a rate-limiting middleware (e.g. express-rate-limit)
אחרת: An application could be subject to an attack resulting in a denial of service where real users receive a degraded or unavailable service.
🔗 Read More: Implement rate limiting
אמ;לק: Never store plain-text secrets in configuration files or source code. Instead, make use of secret-management systems like Vault products, Kubernetes/Docker Secrets, or using environment variables. As a last resort, secrets stored in source control must be encrypted and managed (rolling keys, expiring, auditing, etc). Make use of pre-commit/push hooks to prevent committing secrets accidentally
אחרת: Source control, even for private repositories, can mistakenly be made public, at which point all secrets are exposed. Access to source control for an external party will inadvertently provide access to related systems (databases, apis, services, etc).
🔗 Read More: Secret management
אמ;לק: To prevent SQL/NoSQL injection and other malicious attacks, always make use of an ORM/ODM or a database library that escapes data or supports named or indexed parameterized queries, and takes care of validating user input for expected types. Never just use JavaScript template strings or string concatenation to inject values into queries as this opens your application to a wide spectrum of vulnerabilities. All the reputable Node.js data access libraries (e.g. Sequelize, Knex, mongoose) have built-in protection against injection attacks.
אחרת: Unvalidated or unsanitized user input could lead to operator injection when working with MongoDB for NoSQL, and not using a proper sanitization system or ORM will easily allow SQL injection attacks, creating a giant vulnerability.
🔗 Read More: Query injection prevention using ORM/ODM libraries
אמ;לק: This is a collection of security advice that is not related directly to Node.js - the Node implementation is not much different than any other language. Click read more to skim through.
🔗 Read More: Common security best practices
אמ;לק: Your application should be using secure headers to prevent attackers from using common attacks like cross-site scripting (XSS), clickjacking and other malicious attacks. These can be configured easily using modules like helmet.
אחרת: Attackers could perform direct attacks on your application's users, leading to huge security vulnerabilities
🔗 Read More: Using secure headers in your application
אמ;לק: With the npm ecosystem it is common to have many dependencies for a project. Dependencies should always be kept in check as new vulnerabilities are found. Use tools like npm audit or snyk to track, monitor and patch vulnerable dependencies. Integrate these tools with your CI setup so you catch a vulnerable dependency before it makes it to production.
אחרת: An attacker could detect your web framework and attack all its known vulnerabilities.
🔗 Read More: Dependency security
אמ;לק: Passwords or secrets (e.g. API keys) should be stored using a secure hash + salt function like bcrypt
,scrypt
, or worst case pbkdf2
.
אחרת: Passwords and secrets that are stored without using a secure function are vulnerable to brute forcing and dictionary attacks that will lead to their disclosure eventually.
אמ;לק: Untrusted data that is sent down to the browser might get executed instead of just being displayed, this is commonly referred as a cross-site-scripting (XSS) attack. Mitigate this by using dedicated libraries that explicitly mark the data as pure content that should never get executed (i.e. encoding, escaping)
אחרת: An attacker might store malicious JavaScript code in your DB which will then be sent as-is to the poor clients
אמ;לק: Validate the incoming requests' body payload and ensure it meets expectations, fail fast if it doesn't. To avoid tedious validation coding within each route you may use lightweight JSON-based validation schemas such as jsonschema or joi
אחרת: Your generosity and permissive approach greatly increases the attack surface and encourages the attacker to try out many inputs until they find some combination to crash the application
🔗 Read More: Validate incoming JSON schemas
אמ;לק: When using JSON Web Tokens (for example, with Passport.js), by default there's no mechanism to revoke access from issued tokens. Once you discover some malicious user activity, there's no way to stop them from accessing the system as long as they hold a valid token. Mitigate this by implementing a blocklist of untrusted tokens that are validated on each request.
אחרת: Expired, or misplaced tokens could be used maliciously by a third party to access an application and impersonate the owner of the token.
🔗 Read More: Blocklist JSON Web Tokens
אמ;לק: A simple and powerful technique is to limit authorization attempts using two metrics:
- The first is number of consecutive failed attempts by the same user unique ID/name and IP address.
- The second is number of failed attempts from an IP address over some long period of time. For example, block an IP address if it makes 100 failed attempts in one day.
אחרת: An attacker can issue unlimited automated password attempts to gain access to privileged accounts on an application
🔗 Read More: Login rate limiting
אמ;לק: There is a common scenario where Node.js runs as a root user with unlimited permissions. For example, this is the default behaviour in Docker containers. It's recommended to create a non-root user and either bake it into the Docker image (examples given below) or run the process on this user's behalf by invoking the container with the flag "-u username"
אחרת: An attacker who manages to run a script on the server gets unlimited power over the local machine (e.g. change iptable and re-route traffic to their server)
🔗 Read More: Run Node.js as non-root user
אמ;לק: The bigger the body payload is, the harder your single thread works in processing it. This is an opportunity for attackers to bring servers to their knees without tremendous amount of requests (DOS/DDOS attacks). Mitigate this limiting the body size of incoming requests on the edge (e.g. firewall, ELB) or by configuring express body parser to accept only small-size payloads
אחרת: Your application will have to deal with large requests, unable to process the other important work it has to accomplish, leading to performance implications and vulnerability towards DOS attacks
🔗 Read More: Limit payload size
אמ;לק: eval
is evil as it allows executing custom JavaScript code during run time. This is not just a performance concern but also an important security concern due to malicious JavaScript code that may be sourced from user input. Another language feature that should be avoided is new Function
constructor. setTimeout
and setInterval
should never be passed dynamic JavaScript code either.
אחרת: Malicious JavaScript code finds a way into text passed into eval
or other real-time evaluating JavaScript language functions, and will gain complete access to JavaScript permissions on the page. This vulnerability is often manifested as an XSS attack.
🔗 Read More: Avoid JavaScript eval statements
אמ;לק: Regular Expressions, while being handy, pose a real threat to JavaScript applications at large, and the Node.js platform in particular. A user input for text to match might require an outstanding amount of CPU cycles to process. RegEx processing might be inefficient to an extent that a single request that validates 10 words can block the entire event loop for 6 seconds and set the CPU on 🔥. For that reason, prefer third-party validation packages like validator.js instead of writing your own Regex patterns, or make use of safe-regex to detect vulnerable regex patterns
אחרת: Poorly written regexes could be susceptible to Regular Expression DoS attacks that will block the event loop completely. For example, the popular moment
package was found vulnerable with malicious RegEx usage in November of 2017
🔗 Read More: Prevent malicious RegEx
אמ;לק: Avoid requiring/importing another file with a path that was given as parameter due to the concern that it could have originated from user input. This rule can be extended for accessing files in general (i.e. fs.readFile()
) or other sensitive resource access with dynamic variables originating from user input. Eslint-plugin-security linter can catch such patterns and warn early enough
אחרת: Malicious user input could find its way to a parameter that is used to require tampered files, for example, a previously uploaded file on the file system, or access already existing system files.
🔗 Read More: Safe module loading
אמ;לק: When tasked to run external code that is given at run-time (e.g. plugin), use any sort of 'sandbox' execution environment that isolates and guards the main code against the plugin. This can be achieved using a dedicated process (e.g. cluster.fork()
), serverless environment or dedicated npm packages that act as a sandbox
אחרת: A plugin can attack through an endless variety of options like infinite loops, memory overloading, and access to sensitive process environment variables
🔗 Read More: Run unsafe code in a sandbox
אמ;לק: Avoid using child processes when possible and validate and sanitize input to mitigate shell injection attacks if you still have to. Prefer using child_process.execFile
which by definition will only execute a single command with a set of attributes and will not allow shell parameter expansion.
אחרת: Naive use of child processes could result in remote command execution or shell injection attacks due to malicious user input passed to an unsanitized system command.
🔗 Read More: Be cautious when working with child processes
אמ;לק: An integrated express error handler hides the error details by default. However, great are the chances that you implement your own error handling logic with custom Error objects (considered by many as a best practice). If you do so, ensure not to return the entire Error object to the client, which might contain some sensitive application details
אחרת: Sensitive application details such as server file paths, third party modules in use, and other internal workflows of the application which could be exploited by an attacker, could be leaked from information found in a stack trace
🔗 Read More: Hide error details from client
אמ;לק: Any step in the development chain should be protected with MFA (multi-factor authentication), npm/Yarn are a sweet opportunity for attackers who can get their hands on some developer's password. Using developer credentials, attackers can inject malicious code into libraries that are widely installed across projects and services. Maybe even across the web if published in public. Enabling 2-factor-authentication in npm leaves almost zero chances for attackers to alter your package code.
אחרת: Have you heard about the eslint developer whose password was hijacked?
אמ;לק: Each web framework and technology has its known weaknesses - telling an attacker which web framework we use is a great help for them. Using the default settings for session middlewares can expose your app to module- and framework-specific hijacking attacks in a similar way to the X-Powered-By
header. Try hiding anything that identifies and reveals your tech stack (E.g. Node.js, express)
אחרת: Cookies could be sent over insecure connections, and an attacker might use session identification to identify the underlying framework of the web application, as well as module-specific vulnerabilities
🔗 Read More: Cookie and session security
אמ;לק: The Node process will crash when errors are not handled. Many best practices even recommend to exit even though an error was caught and got handled. Express, for example, will crash on any asynchronous error - unless you wrap routes with a catch clause. This opens a very sweet attack spot for attackers who recognize what input makes the process crash and repeatedly send the same request. There's no instant remedy for this but a few techniques can mitigate the pain: Alert with critical severity anytime a process crashes due to an unhandled error, validate the input and avoid crashing the process due to invalid user input, wrap all routes with a catch and consider not to crash when an error originated within a request (as opposed to what happens globally)
אחרת: This is just an educated guess: given many Node.js applications, if we try passing an empty JSON body to all POST requests - a handful of applications will crash. At that point, we can just repeat sending the same request to take down the applications with ease
אמ;לק: Redirects that do not validate user input can enable attackers to launch phishing scams, steal user credentials, and perform other malicious actions.
אחרת: If an attacker discovers that you are not validating external, user-supplied input, they may exploit this vulnerability by posting specially-crafted links on forums, social media, and other public places to get users to click it.
🔗 Read More: Prevent unsafe redirects
אמ;לק: Precautions should be taken to avoid the risk of accidentally publishing secrets to public npm registries. An .npmignore
file can be used to ignore specific files or folders, or the files
array in package.json
can act as an allow list.
אחרת: Your project's API keys, passwords or other secrets are open to be abused by anyone who comes across them, which may result in financial loss, impersonation, and other risks.
🔗 Read More: Avoid publishing secrets
אמ;לק: Use your preferred tool (e.g. npm outdated
or npm-check-updates) to detect installed outdated packages, inject this check into your CI pipeline and even make a build fail in a severe scenario. For example, a severe scenario might be when an installed package is 5 patch commits behind (e.g. local version is 1.3.1 and repository version is 1.3.8) or it is tagged as deprecated by its author - kill the build and prevent deploying this version
אחרת: Your production will run packages that have been explicitly tagged by their author as risky
אמ;לק: Import or require built-in Node.js modules using the 'node protocol' syntax:
import { functionName } from "node:module"; // note that 'node:' prefix
For example:
import { createServer } from "node:http";
This style ensures that there is no ambiguity with global npm packages and makes it clear for the reader that the code refers to a well-trusted official module. This style can be enforced with the eslint rule 'prefer-node-protocol'
אחרת: Using the import syntax without 'node:' prefix opens the door for typosquatting attacks where one could mistakenly mistype a module name (e.g., 'event' instead of 'events) and get a malicious package that was built only to trick users into installing them
Our contributors are working on this section. Would you like to join?
אמ;לק: Avoid CPU intensive tasks as they will block the mostly single-threaded Event Loop and offload those to a dedicated thread, process or even a different technology based on the context.
אחרת: As the Event Loop is blocked, Node.js will be unable to handle other request thus causing delays for concurrent users. 3000 users are waiting for a response, the content is ready to be served, but one single request blocks the server from dispatching the results back
🔗 Read More: Do not block the event loop
אמ;לק: It's often more penalising to use utility libraries like lodash
and underscore
over native methods as it leads to unneeded dependencies and slower performance.
Bear in mind that with the introduction of the new V8 engine alongside the new ES standards, native methods were improved in such a way that it's now about 50% more performant than utility libraries.
אחרת: You'll have to maintain less performant projects where you could have simply used what was already available or dealt with a few more lines in exchange of a few more files.
🔗 Read More: Native over user land utils
🏅 Many thanks to Bret Fisher from whom we learned many of the following practices
אמ;לק: Use multi-stage build to copy only necessary production artifacts. A lot of build-time dependencies and files are not needed for running your application. With multi-stage builds these resources can be used during build while the runtime environment contains only what's necessary. Multi-stage builds are an easy way to get rid of overweight and security threats.
אחרת: Larger images will take longer to build and ship, build-only tools might contain vulnerabilities and secrets only meant for the build phase might be leaked.
FROM node:14.4.0 AS build
COPY . .
RUN npm ci && npm run build
FROM node:slim-14.4.0
USER node
EXPOSE 8080
COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm ci --production
CMD [ "node", "dist/app.js" ]
🔗 Read More: Use multi-stage builds
אמ;לק: Use CMD ['node','server.js']
to start your app, avoid using npm scripts which don't pass OS signals to the code. This prevents problems with child-processes, signal handling, graceful shutdown and having zombie processes
Update: Starting from npm 7, npm claim to pass signals. We follow and will update accordingly
אחרת: When no signals are passed, your code will never be notified about shutdowns. Without this, it will lose its chance to close properly possibly losing current requests and/or data
Read More: Bootstrap container using node command, avoid npm start
אמ;לק: When using a Docker run time orchestrator (e.g., Kubernetes), invoke the Node.js process directly without intermediate process managers or custom code that replicate the process (e.g. PM2, Cluster module). The runtime platform has the highest amount of data and visibility for making placement decision - It knows best how many processes are needed, how to spread them and what to do in case of crashes
אחרת: Container keeps crashing due to lack of resources will get restarted indefinitely by the process manager. Should Kubernetes be aware of that, it could relocate it to a different roomy instance
🔗 Read More: Let the Docker orchestrator restart and replicate processes
TL;DR: Include a .dockerignore
file that filters out common secret files and development artifacts. By doing so, you might prevent secrets from leaking into the image. As a bonus the build time will significantly decrease. Also, ensure not to copy all files recursively rather explicitly choose what should be copied to Docker
Otherwise: Common personal secret files like .env
, .aws
and .npmrc
will be shared with anybody with access to the image (e.g. Docker repository)
🔗 Read More: Use .dockerignore
אמ;לק: Although Dev-Dependencies are sometimes needed during the build and test life-cycle, eventually the image that is shipped to production should be minimal and clean from development dependencies. Doing so guarantees that only necessary code is shipped and the amount of potential attacks (i.e. attack surface) is minimized. When using multi-stage build (see dedicated bullet) this can be achieved by installing all dependencies first and finally running npm ci --production
אחרת: Many of the infamous npm security breaches were found within development packages (e.g. eslint-scope)
🔗 Read More: Remove development dependencies
אמ;לק: Handle the process SIGTERM event and clean-up all existing connection and resources. This should be done while responding to ongoing requests. In Dockerized runtimes, shutting down containers is not a rare event, rather a frequent occurrence that happen as part of routine work. Achieving this demands some thoughtful code to orchestrate several moving parts: The load balancer, keep-alive connections, the HTTP server and other resources
אחרת: Dying immediately means not responding to thousands of disappointed users
🔗 Read More: Graceful shutdown
אמ;לק: Always configure a memory limit using both Docker and the JavaScript runtime flags. The Docker limit is needed to make thoughtful container placement decision, the --v8's flag max-old-space is needed to kick off the GC on time and prevent under utilization of memory. Practically, set the v8's old space memory to be a just bit less than the container limit
אחרת: The docker definition is needed to perform thoughtful scaling decision and prevent starving other citizens. Without also defining the v8's limits, it will under utilize the container resources - Without explicit instructions it crashes when utilizing ~50-60% of its host resources
🔗 Read More: Set memory limits using Docker only
אמ;לק: Rebuilding a whole docker image from cache can be nearly instantaneous if done correctly. The less updated instructions should be at the top of your Dockerfile and the ones constantly changing (like app code) should be at the bottom.
אחרת: Docker build will be very long and consume lot of resources even when making tiny changes
🔗 Read More: Leverage caching to reduce build times
אמ;לק: Specify an explicit image digest or versioned label, never refer to latest
. Developers are often led to believe that specifying the latest
tag will provide them with the most recent image in the repository however this is not the case. Using a digest guarantees that every instance of the service is running exactly the same code.
In addition, referring to an image tag means that the base image is subject to change, as image tags cannot be relied upon for a deterministic install. Instead, if a deterministic install is expected, a SHA256 digest can be used to reference an exact image.
אחרת: A new version of a base image could be deployed into production with breaking changes, causing unintended application behaviour.
🔗 Read More: Understand image tags and use the "latest" tag with caution
אמ;לק: Large images lead to higher exposure to vulnerabilities and increased resource consumption. Using leaner Docker images, such as Slim and Alpine Linux variants, mitigates this issue.
אחרת: Building, pushing, and pulling images will take longer, unknown attack vectors can be used by malicious actors and more resources are consumed.
🔗 Read More: Prefer smaller images
אמ;לק: Avoid secrets leaking from the Docker build environment. A Docker image is typically shared in multiple environment like CI and a registry that are not as sanitized as production. A typical example is an npm token which is usually passed to a dockerfile as argument. This token stays within the image long after it is needed and allows the attacker indefinite access to a private npm registry. This can be avoided by coping a secret file like .npmrc
and then removing it using multi-stage build (beware, build history should be deleted as well) or by using Docker build-kit secret feature which leaves zero traces
אחרת: Everyone with access to the CI and docker registry will also get access to some precious organization secrets as a bonus
🔗 Read More: Clean-out build-time secrets
אמ;לק: Besides checking code dependencies vulnerabilities also scan the final image that is shipped to production. Docker image scanners check the code dependencies but also the OS binaries. This E2E security scan covers more ground and verifies that no bad guy injected bad things during the build. Consequently, it is recommended running this as the last step before deployment. There are a handful of free and commercial scanners that also provide CI/CD plugins
אחרת: Your code might be entirely free from vulnerabilities. However it might still get hacked due to vulnerable version of OS-level binaries (e.g. OpenSSL, TarBall) that are commonly being used by applications
🔗 Read More: Scan the entire image before production
אמ;לק: After installing dependencies in a container remove the local cache. It doesn't make any sense to duplicate the dependencies for faster future installs since there won't be any further installs - A Docker image is immutable. Using a single line of code tens of MB (typically 10-50% of the image size) are shaved off
אחרת: The image that will get shipped to production will weigh 30% more due to files that will never get used
🔗 Read More: Clean NODE_MODULE cache
אמ;לק: This is a collection of Docker advice that is not related directly to Node.js - the Node implementation is not much different than any other language. Click read more to skim through.
🔗 Read More: Generic Docker practices
אמ;לק: Linting your Dockerfile is an important step to identify issues in your Dockerfile which differ from best practices. By checking for potential flaws using a specialised Docker linter, performance and security improvements can be easily identified, saving countless hours of wasted time or security issues in production code.
אחרת: Mistakenly the Dockerfile creator left Root as the production user, and also used an image from unknown source repository. This could be avoided with with just a simple linter.
🔗 Read More: Lint your Dockerfile
To maintain this guide and keep it up to date, we are constantly updating and improving the guidelines and best practices with the help of the community. You can follow our milestones and join the working groups if you want to contribute to this project
All translations are contributed by the community. We will be happy to get any help with either completed, ongoing or new translations!
Brazilian Portuguese - Courtesy of Marcelo Melo
Chinese - Courtesy of Matt Jin
Russian - Courtesy of Alex Ivanov
Polish - Courtesy of Michal Biesiada
Japanese - Courtesy of Yuki Ota, Yuta Azumi
Basque - Courtesy of Ane Diaz de Tuesta & Joxefe Diaz de Tuesta
French (Discussion)
Hebrew (Discussion)
Korean - Courtesy of Sangbeom Han (Discussion)
Spanish (Discussion)
Turkish (Discussion)
Meet the steering committee members - the people who work together to provide guidance and future direction to the project. In addition, each member of the committee leads a project tracked under our GitHub projects.
Independent Node.js consultant who works with customers in the USA, Europe, and Israel on building large-scale Node.js applications. Many of the best practices above were first published at goldbergyoni.com. Reach Yoni at @goldbergyoni or me@goldbergyoni.com
Full Stack Software Engineer / Developer specializing in Security, DevOps/DevSecOps, and ERP Integrations.
Full Stack Developer who knows how to exit from Vim and loves Architecture, Virtualization and Security.
If you've ever wanted to contribute to open source, now is your chance! See the contributing docs for more information.
Thanks goes to these wonderful people who have contributed to this repository!
💻 full-stack web engineer, Node.js & GraphQL enthusiast
Full Stack Developer & Site Reliability Engineer based in New Zealand, interested in web application security, and architecting and building Node.js applications to perform at global scale.
Independent full-stack developer with a taste for Ops and automation.
Deep specialist in JavaScript and its ecosystem — React, Node.js, TypeScript, GraphQL, MongoDB, pretty much anything that involves JS/JSON in any layer of the system — building products using the web platform for the world’s most recognized brands. Individual Member of the Node.js Foundation.