अच्छे API डिज़ाइन के बारे में मैं जो कुछ जानता हूँ
(seangoedecke.com)- सॉफ्टवेयर इंजीनियरिंग में API एक मुख्य टूल है, और अच्छे API की वांछनीय विशेषता यह है कि वह इतना परिचित और सरल हो कि लगभग उबाऊ लगे
- API एक बार सार्वजनिक हो जाए तो उसे बदलना मुश्किल होता है, इसलिए यूज़र environment को न तोड़ने का सिद्धांत (WE DO NOT BREAK USERSPACE) महत्वपूर्ण है
- यदि बदलाव अपरिहार्य हो, तो versioning की ज़रूरत पड़ती है, लेकिन यह जटिलता और maintenance cost को बहुत बढ़ाने वाली एक ज़रूरी बुराई है
- API की गुणवत्ता अंततः प्रोडक्ट के अपने मूल्य पर निर्भर करती है, और खराब ढंग से डिज़ाइन किए गए प्रोडक्ट के लिए अच्छा API बनाना कठिन होता है
- स्थिरता और विस्तारयोग्यता के लिए API key आधारित authentication, idempotency, rate limit, cursor-based pagination आदि पर विचार करना चाहिए
प्रस्तावना: API डिज़ाइन का महत्व और संदर्भ
- आधुनिक सॉफ्टवेयर इंजीनियर के प्रमुख कामों में से एक API के साथ इंटरैक्ट करना है
- लेखक के पास भी REST, GraphQL, command-line tools आदि जैसे विभिन्न रूपों में public और internal API को डिज़ाइन/इम्प्लीमेंट/उपयोग करने का अनुभव है
- मौजूदा API डिज़ाइन सलाह अक्सर जटिल अवधारणाओं (REST की परिभाषा, HATEOAS आदि) पर अत्यधिक केंद्रित रहती है
- यह लेख वास्तविक अनुभव के आधार पर व्यावहारिक API डिज़ाइन सिद्धांतों को व्यवस्थित करता है
परिचितपन और लचीलापन का संतुलन: अच्छे API की पहली शर्त
- अच्छा API एक 'साधारण और उबाऊ' API होता है, यानी उसका इस्तेमाल पहले देखे गए API जैसा महसूस होना चाहिए
- यूज़र API से ज़्यादा अपने लक्ष्य को पूरा करने पर ध्यान देता है, इसलिए low barrier वाली डिज़ाइन ज़रूरी है
- एक बार public हो जाने के बाद API को बदलना बहुत मुश्किल हो जाता है, इसलिए शुरुआती डिज़ाइन चरण में सावधानी चाहिए
- डेवलपर जहाँ संभव हो API को सरल रखना चाहते हैं, वहीं लंबी अवधि की flexibility बचाए रखने की चिंता भी बनी रहती है
- नतीजतन, परिचितपन और दीर्घकालिक flexibility के बीच संतुलन ही मुख्य चुनौती है
यूज़रस्पेस को कभी न तोड़ें (WE DO NOT BREAK USERSPACE)
- मौजूदा response structure में fields जोड़ना ज़्यादातर मामलों में समस्या नहीं बनता
- लेकिन field हटाना, type या structure बदलना सभी consumer code को तोड़ सकता है
- API maintainers की यह ज़िम्मेदारी है कि वे मौजूदा यूज़र्स के software को जानबूझकर न तोड़ें
- HTTP के
refererheader की वर्तनी गलती तक न सुधारे जाने की वजह भी यूज़रस्पेस को बचाए रखने की संस्कृति है
API को तोड़े बिना बदलना: versioning रणनीति
- केवल अनिवार्य होने पर ही API में breaking changes की अनुमति देनी चाहिए, और ऐसे में versioning ही सही रास्ता है
- पुराने और नए version को एक साथ चलाते हुए gradual migration के लिए प्रेरित करना चाहिए
- version identifier को URL(
/v1/), header आदि कई तरीकों से इस्तेमाल किया जा सकता है, और यूज़र अपनी गति से migrate कर सकते हैं - versioning के साथ बहुत बड़ा maintenance cost (endpoint बढ़ना, testing, support) और यूज़र confusion जैसी कमियाँ आती हैं
- Stripe की तरह अंदरूनी translation layer रखी जाए तब भी मूल जटिलता से बचा नहीं जा सकता
- API versioning अपनाना आख़िरी उपाय होना चाहिए
API की सफलता पूरी तरह प्रोडक्ट के मूल्य पर निर्भर करती है
- API मूलतः वास्तविक business product का सिर्फ interface है
- OpenAI, Twilio जैसी API में भी आखिरकार यूज़र को चाहिए वह functionality जो API उपलब्ध कराती है
- यदि प्रोडक्ट मूल्यवान है, तो API असुविधाजनक होने पर भी लोग उसका उपयोग करेंगे
- API quality एक तरह की margin विशेषता है: यह तब चयन का कारक बनती है जब मूल प्रतिस्पर्धात्मक क्षमता लगभग समान हो
- इसके विपरीत, बिल्कुल API न होना तकनीकी यूज़र्स के लिए बड़ा अवरोध है
अगर प्रोडक्ट डिज़ाइन खराब है, तो API भी अच्छा नहीं बन सकता
- तकनीकी रूप से परिष्कृत API होने पर भी यदि प्रोडक्ट की market viability नहीं है, तो उसका अर्थ सीमित रह जाता है
- इससे भी महत्वपूर्ण बात यह है कि यदि बुनियादी resource structure अलॉजिकल या अक्षम है, तो वह API में भी दिखेगा
- उदाहरण के लिए, comments को linked list में स्टोर करने वाली system में RESTful डिज़ाइन भी स्वाभाविक रूप से निकालना कठिन हो जाता है
- UI में जो तकनीकी समस्याएँ छिप सकती हैं, वे API में खुलकर सामने आ जाती हैं और यूज़र पर सिस्टम की अनावश्यक समझ थोपती हैं
authentication और यूज़र्स की विविधता
- long-lived API key आधारित authentication को अनिवार्य रूप से support करना चाहिए
- OAuth जैसी अधिक secure विधि का अतिरिक्त support दिया जाए तब भी, API key का entry barrier बहुत कम होता है
- API consumer सिर्फ इंजीनियर नहीं होते; कई non-developer भी होते हैं (sales, planning, students, hobby developers आदि)
- कठिन या जटिल authentication आवश्यकताएँ (जैसे OAuth) गैर-विशेषज्ञ यूज़र्स के लिए बाधा बनती हैं
idempotency और retry handling
- action-oriented requests (जैसे payment, state change आदि) में failure होने पर retry की सुरक्षा बहुत महत्वपूर्ण है
- idempotency का अर्थ है यह सुनिश्चित करना कि एक ही request कई बार भेजी जाए तब भी परिणाम सिर्फ एक बार ही प्रोसेस हो
- मानक तरीका यह है कि duplicate processing रोकने के लिए idempotency key को parameter या header के रूप में भेजा जाए
- idempotency key को स्टोर करने के लिए Redis जैसे सरल key/value store पर्याप्त हैं, और अधिकांश मामलों में periodic expiry भी ठीक रहती है
- read/delete requests (REST शैली) में आमतौर पर इसकी ज़रूरत नहीं होती
API सुरक्षा और rate limiting
- कोड के जरिए API requests, यूज़र के मैनुअल ऑपरेशन की तुलना में कहीं तेज़ी से हो सकती हैं
- अनजाने में deploy की गई एक API भी किसी अप्रत्याशित उपयोग (जैसे बड़े पैमाने के chat system) में लग सकती है
- rate limit अनिवार्य है, और महँगे operations पर इसे और सख़्ती से लागू किया जाना चाहिए
- किसी विशेष ग्राहक के लिए अस्थायी API disablement (killswitch) को भी एक विकल्प के रूप में सोचना चाहिए
- response headers (
X-Limit-Remaining,Retry-Afterआदि) के जरिए rate limit जानकारी देनी चाहिए
pagination रणनीति
- बड़े dataset (जैसे लाखों tickets) को कुशलता से लौटाने के लिए pagination अनिवार्य है
- offset-based pagination सरल है, लेकिन बड़े data पर धीरे-धीरे धीमी हो जाती है
- cursor-based pagination query performance घटाए बिना बहुत बड़े dataset पर भी प्रभावी रहती है
- cursor-based तरीका implementation और उपयोग दोनों में थोड़ा कठिन है, लेकिन लंबी अवधि में यह अनिवार्य बदलाव साबित हो सकता है
- response में
next_pagefield आदि शामिल करके अगले request के cursor को स्पष्ट रूप से बताना समझदारी है
optional fields और GraphQL पर दृष्टिकोण
- महँगे या धीमे fields को default response से बाहर रखना चाहिए और ज़रूरत होने पर ही selectively जोड़ना चाहिए
includesparameter आदि से related data शामिल किया जा सकता है- GraphQL में data structure flexibility का लाभ है, लेकिन non-developer accessibility में कमी, caching/edge case की जटिलता, backend implementation की कठिनाई जैसी समस्याएँ भी हैं
- व्यावहारिक अनुभव के आधार पर, GraphQL को सिर्फ तभी अपनाना उचित है जब इसकी वास्तव में आवश्यकता हो
internal API की विशेषताएँ
- internal API की परिस्थितियाँ external API (public API) से कई मायनों में अलग होती हैं
- इनके consumer अधिकांशतः पेशेवर software engineers होते हैं, इसलिए अधिक जटिल authentication या breaking changes संभव हो सकते हैं
- फिर भी, idempotency, दुर्घटना-निवारण और operational burden को कम करने वाले डिज़ाइन सिद्धांत यहाँ भी लागू होते हैं
सारांश
- API को बदलना कठिन और उपयोग करना आसान होना चाहिए
- यूज़रस्पेस को न तोड़ना API maintainers का सबसे महत्वपूर्ण कर्तव्य है
- API versioning की लागत बहुत अधिक है, इसलिए इसका उपयोग केवल आख़िरी उपाय के रूप में होना चाहिए
- अंततः API की गुणवत्ता प्रोडक्ट के मूल मूल्य से तय होती है
- खराब ढंग से डिज़ाइन किए गए प्रोडक्ट की सीमाएँ API स्तर पर सुधार करके भी पूरी तरह दूर नहीं की जा सकतीं
- सरल authentication का support, ज़रूरी action requests में idempotency, और rate limiting/pagination जैसे stability उपाय महत्वपूर्ण हैं
- internal API के लिए उपयोग और लक्षित यूज़र के अनुसार रणनीति अलग हो सकती है, लेकिन सावधानीपूर्वक डिज़ाइन की आवश्यकता बनी रहती है
- REST, JSON जैसे format या OpenAPI आदि मूल मुद्दे नहीं हैं; स्पष्ट documentation अधिक महत्वपूर्ण है
1 टिप्पणियां
Hacker News राय
"userspace को कभी मत तोड़ो" वाली सलाह मशहूर है, लेकिन यह अच्छी तरह बताया गया है कि इसका उल्टा पहलू भी है। यानी, "kernel API बिना चेतावनी के टूट सकते हैं"। असली बात "किसी भी API को कभी मत तोड़ो" नहीं, बल्कि यह सूक्ष्म संतुलन है कि "सिर्फ वही हिस्से कभी मत तोड़ो जिन्हें तुमने stable घोषित किया है"
भले ही Linux kernel userspace को न तोड़े, GNU libc userspace compatibility को काफ़ी बार तोड़ देता है। इसलिए नतीजे में Linux user space, kernel developers की पूरी कोशिशों के बावजूद, अक्सर टूट जाता है। नए libc version पर built programs और libraries कभी-कभी पुराने libc पर ठीक से नहीं चलते, इसलिए practically सभी components को एक साथ upgrade करना पड़ता है। थोड़ी विडंबना यह है कि Windows ने यह समस्या redistributable मॉडल से दशकों पहले ही हल कर ली थी
Linux में प्रसिद्ध रूप से stable public driver API नहीं है, और सुना है कि यही Google के Fuschia OS विकसित करने की प्रेरणाओं में से एक था। इस तरह Linux, user space और hardware—दोनों के लिए अलग-अलग दिशा वाली नीति रखता है
लगता है कि लेखक को version-based API ज़्यादा पसंद नहीं, लेकिन मैं तो हमेशा recommend करता हूँ कि app बनाते समय शुरू से ही versioning ज़रूर अपनाओ। भविष्य का अनुमान नहीं लगाया जा सकता, इसलिए कभी न कभी बाहरी कारणों से breaking change तुम्हारे साथ भी होना तय है
दरअसल मुझे लगता है कि लेखक ने भी versioning की सिफारिश की है। मुख्य लेख में कहा गया था कि "versioning APIs को ज़िम्मेदारी से बदलने का तरीका है", तो अंततः यह versioning को बढ़ावा देने जैसा ही है। बस, नए version पर जाना आख़िरी उपाय होना चाहिए
मैं इस बात से सहमत हूँ कि endpoint पर ज़बरदस्ती "v1" नहीं लगाना चाहिए। व्यवहार में API के बढ़ने पर पहले existing endpoint में fields या options जोड़कर backward compatibility बनाए रखने की कोशिश की जाती है। और जब पूरी तरह incompatible काम की ज़रूरत पड़ती है, तब आमतौर पर endpoint का नाम ही नया रखा जाता है और बिल्कुल नया endpoint बनाया जाता है (/v2 नहीं)। अगर पूरा API ही बदलना पड़े, तो पुरानी service retire करके नाम से ही नई अलग service launch की जाती है। 25 साल काम करते हुए मैंने सिर्फ़ एक बार ऐसी service देखी है जहाँ "/v1" और "/v2" साथ-साथ इस्तेमाल हो रहे थे
मुझे नहीं लगता कि लेखक का मतलब शुरू से endpoint में /v1 न डालने का था। मुद्दा यह है कि /v2 आने की नौबत ही न आए, इसके लिए पूरी कोशिश करनी चाहिए। /v2 आ गया तो हर bug fix में दोनों तरफ़ code बदलना पड़ता है, conditional branches तेज़ी से बढ़ती हैं और codebase spaghetti की तरह उलझ जाता है। आख़िरकार multiple versions support करने की नौबत आने का मतलब है कि शुरुआती /v1 design में future compatibility के लिए पर्याप्त सोच नहीं थी
बाद में versioning जोड़ना भी ठीक है। उदाहरण के लिए, पहले /api/posts से शुरू करो और अगला version /api/v2/posts के रूप में जोड़ दो, इतना काफ़ी है
मैं शुरू से version ठूँस देने वाले तरीके से सहमत नहीं हूँ। ऐसा करने पर multiple versions सचमुच ज़्यादा इस्तेमाल होने लगते हैं, और मुझे नहीं लगता कि वह बेहतर है
यह लेख बहुत उपयोगी था। मैं इसमें एक सलाह और जोड़ूँगा। API documentation जितनी मुश्किल से मिलती है, API quality उतनी ही ख़राब होने की संभावना होती है। अगर documentation पाने के लिए contract sign करना पड़े, तो मानकर चलो कि उस API की quality बहुत ख़राब होगी
इसमें कहा गया कि idempotency key को comment table में अलग से रखने के बजाय Redis जैसे key/value store में रखो, लेकिन मुझे शक है कि क्या यह तरीका हर failure case में पक्का idempotency दे सकता है। मान लो server
SET key 1 NXजैसी conditional write करता है और देखता है कि key पहले से मौजूद है, तो उसे comment creation skip कर देनी चाहिए। लेकिन उस समय यह भी हो सकता है कि पिछली request वास्तव में DB में commit ही न हुई हो। idempotency key का storage असली operation के साथ transaction unit में commit होना चाहिए, और ज़रूरत पड़े तो rollback भी हो सके। अंततः idempotency key का सार यही है कि वह ‘इस operation या request की unique ID’ हो। जैसे “comment creation”, “comment update” आदि के लिए अलग resource-level identifier होना चाहिएcursor-based pagination का फ़ायदा यह है कि user के page load करने और ‘Next’ दबाने के बीच अगर नए items जुड़ भी जाएँ, तो उसे पहले से देखी हुई items दोबारा नहीं दिखेंगी। cursor method पिछले page के आख़िरी object ID को record करके उसके बाद के items देता है, इसलिए यह infinite scroll में ख़ास तौर पर उपयोगी है। दूसरी ओर, cursor-based pagination की कमी यह है कि “Nth page पर jump” जैसी सुविधा बनाना मुश्किल होता है
आजकल "API" सुनते ही ज़्यादातर लोग web app को request भेजना, parameters और headers set करना, और data लाना सोचते हैं, लेकिन मूल रूप से API का मतलब "Application Programming Interface" है, यानी ‘application program का interface’। यह शब्द पहली बार 1940 के दशक में इस्तेमाल हुआ था, और उसके बाद 1990 के दशक तक लगभग इसी अर्थ में इस्तेमाल होता रहा। API का इतिहास 80 साल से भी पुराना है, और इस पर बहुत पुराना material भी मौजूद है। उस समय programmers किन समस्याओं से जूझते थे और उन्हें कैसे हल करते थे, इस पर सोचने से आज भी काफ़ी सीख मिल सकती है
मैं इस बात से सहमत नहीं हूँ कि internal users को बस 'users' समझ लेना काफ़ी है। भले ही वे ज़्यादा technical लोग हों और programmer होने की संभावना ज़्यादा हो, फिर भी वे व्यस्त होते हैं और अपने projects पर केंद्रित रहते हैं, इसलिए API changes के हिसाब से खुद को ढालने के लिए उनके पास समय या गुंजाइश कम हो सकती है। इसलिए अगर संभव हो, तो public release से पहले team के अंदर अच्छी तरह "dogfooding" (वास्तविक उपयोग) testing करना महत्वपूर्ण है। एक बार बाहर public कर दिया, तो ‘userspace को नहीं तोड़ेंगे’ वाला वादा निभाना ही होगा
internal users के लिए आमतौर पर ऐसे instrumentation tools बने होते हैं जिनसे उन्हें सीधे contact करके migration करवाया जा सकता है। इसी वजह से API version deprecation भी संभव हो पाता है, इसलिए रणनीतिक रूप से versioning अपनाना काफ़ी आकर्षक है। मैंने खुद API versioning पर काम किया है, और इसे न इस्तेमाल करने वाले संगठनों की तुलना में इसका स्पष्ट फ़ायदा देखा है
मुझे लगता है कि versioning इस समस्या को हल करने में मदद करती है। internal users का ख़याल रखने के सबसे अच्छे तरीकों में से एक है spec पर मिलकर काम करना, और उस spec के work-in-progress versions भी stakeholders के साथ साझा करना। भले ही documentation लगातार update होती रहे, फिर भी एक baseline होने से अंदर और बाहर—दोनों तरफ़ feedback लेना आसान हो जाता है, और अगर policy-level conflicts से बचा जाए तो यह बहुत उपयोगी साबित हो सकता है
Redis में idempotency key रखने के बजाय, अगर संभव हो, तो जिस same transaction में वास्तविक data लिखा जा रहा है, उसी में idempotency key भी साथ में store करना ज़्यादा भरोसेमंद है
"userspace को कभी मत तोड़ो" वाली चेतावनी सचमुच बहुत महत्वपूर्ण है। हाल के समय में Spotify, Reddit, Twitter आदि को इस सिद्धांत की अनदेखी करते देखना निराशाजनक रहा
संदर्भ के लिए https://jcs.org/2023/07/12/api लिंक में API से जुड़ी अच्छी recommendations काफ़ी अच्छी तरह संकलित हैं, उन्हें भी साथ में देखना उपयोगी होगा