بلاگز / FastAPI Dependency Injection: ایک مکمل گائیڈ

FastAPI Dependency Injection: ایک مکمل گائیڈ

شائع ہوا
27 اگست، 2026
مصنف
Faizan Nadeem
ٹیگز
FastAPI Python Backend Development Software Architecture
ایک شخص کے ہاتھ دو puzzle pieces کو ایک ساتھ تھامے ہوئے ہیں، جو جڑنے ہی والے ہیں
تصویر از Vardan Papikyan، Unsplash پر

Depends کو ہر FastAPI tutorial میں “current user حاصل کرنے کا طریقہ” کہہ کر متعارف کروایا جاتا ہے، اور زیادہ تر developers کبھی اس ایک use case سے آگے نہیں دیکھتے۔ یہ افسوس کی بات ہے، کیونکہ Depends واقعی ایک dependency injection system ہے — یہ ایک graph resolve کرتا ہے، results کو ہر request کے لیے cache کرتا ہے، اور setup/teardown lifecycles کو manage کرتا ہے — اور اگر اسے صرف auth checks کے لیے ایک decorator سمجھا جائے تو آپ ان patterns سے محروم رہ جاتے ہیں جو بڑھتے ہوئے FastAPI codebase کو حقیقتاً testable رکھتے ہیں: database sessions جو handler کے exception raise کرنے پر بھی خود کو درست طریقے سے clean up کریں، permission checks جو ہر route پر copy-paste ہونے کے بجائے چھوٹے checks سے compose ہوں، اور request-scoped values جو ایک ہی request میں کتنی ہی dependencies کو کیوں نہ درکار ہوں، صرف ایک بار compute ہوں۔

دوسری طرف، Depends اتنا flexible بھی ہے کہ اس سے واقعی خراب architecture بھی بن سکتا ہے، اور اس کا سب سے عام طریقہ یہ ہے کہ endpoint function کو ایک عام Python function سمجھ لیا جائے جسے آپ logic “reuse” کرنے کے لیے کسی دوسرے endpoint سے بس call کر لیں۔ یہ compile ہو جاتا ہے۔ demo میں اکثر چل بھی جاتا ہے۔ لیکن یہ FastAPI کے dependency system کے پورے model کو توڑ دیتا ہے، اور یہی وہ واحد anti-pattern ہے جو ان codebases کو الگ کرتا ہے جہاں Depends productivity multiplier بنتا ہے، ان codebases سے جہاں یہ پراسرار bugs کا ذریعہ بن جاتا ہے۔ یہ post دونوں پہلوؤں کا احاطہ کرتی ہے: وہ patterns جنہیں شعوری طور پر استعمال کرنا چاہیے، اور وہ anti-patterns جن سے جان بوجھ کر بچنا چاہیے۔

آپ یہ سیکھیں گے:

  • FastAPI حقیقت میں Depends graph کو کیسے resolve کرتا ہے، کس ترتیب سے، اور یہ کتنی گہرائی تک nest ہو سکتا ہے
  • کیوں yield-based dependencies ہر اس چیز کے لیے درست tool ہیں جسے guaranteed teardown چاہیے
  • per-request dependency caching کیسے کام کرتی ہے، اور کب خاموشی سے لاگو نہیں ہوتی
  • shared dependencies رکھنے کی درست جگہ کہاں ہے تاکہ ہر router logic duplicate کیے بغیر انہیں استعمال کر سکے
  • کیوں ایک endpoint function کو دوسرے سے call کرنا ایک design smell ہے، اور اس کے بجائے کیا کرنا چاہیے
  • parameterized اور reusable checks کے لیے class-based dependencies

فہرستِ مضامین

  1. بنیادی باتیں
  2. Depends resolution حقیقت میں کیسے کام کرتی ہے
  3. Yield-based dependencies اور teardown
  4. ایک request کے اندر dependency caching
  5. Shared dependencies درست طریقے سے
  6. Anti-pattern: ایک endpoint کو دوسرے سے call کرنا
  7. Class-based dependencies
  8. dependency_overrides کے ساتھ testing
  9. عام pitfalls
  10. Production best practices

بنیادی باتیں

Depends حقیقت میں کیا ہے

Depends کسی parameter کو اس چیز کے طور پر mark کرتا ہے جسے آپ کے function چلنے سے پہلے FastAPI آپ کے لیے resolve کرے — کسی دوسرے function (یا callable) کو call کر کے، جس کے اپنے Depends parameters بھی ہو سکتے ہیں، اور پھر اس کا result pass کر کے۔ یہ dependency injection اسی معنی میں ہے جس میں یہ اصطلاح دوسرے frameworks میں استعمال ہوتی ہے: آپ کا function اعلان کرتا ہے کہ اسے کیا چاہیے، اور کوئی دوسرا component اسے construct کرنے کا ذمہ دار ہوتا ہے۔

from fastapi import Depends

def get_query_token(token: str) -> str:
    return token

@app.get("/items")
async def read_items(token: str = Depends(get_query_token)):
    return {"token": token}

یہ ایک dependency کے لیے چھوٹی سی سہولت لگتی ہے، مگر یہ compose ہوتی ہے: get_query_token خود بھی کسی اور چیز پر depend کر سکتا ہے، اور FastAPI پورا chain ترتیب کے ساتھ resolve کرتا ہے، اس سے پہلے کہ آپ کا route handler چلے۔

یہ auth سے آگے کیوں اہم ہے

ہر tutorial کی پہلی مثال get_current_user ہوتی ہے، جس سے Depends ایسا لگتا ہے جیسے یہ صرف authentication کے لیے موجود ہو۔ عملی طور پر یہی mechanism database sessions، pagination parameters، feature-flag checks، request-scoped loggers، اور rate limiting کے لیے درست tool ہے — یعنی ہر وہ چیز جو “وہ setup work جس کی handler کو ضرورت ہو، جو مستقل انداز میں compute ہو، اور ممکن ہو کہ بعد میں tear down بھی ہو” ایک Depends candidate ہے، صرف auth check نہیں۔

Depends resolution حقیقت میں کیسے کام کرتی ہے

FastAPI ہر request کے لیے ایک dependency graph بناتا ہے، ہر Depends parameter کو recursively walk کر کے، اور پہلے leaf nodes resolve کرتا ہے۔ مثال کے طور پر:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def get_current_user(token: str = Depends(get_query_token), db=Depends(get_db)):
    return db.query(User).filter_by(token=token).first()

@app.get("/profile")
async def profile(user=Depends(get_current_user)):
    return user

/profile کو call کرنے پر FastAPI پہلے get_query_token اور get_db کو resolve کرتا ہے (کیونکہ ان کی مزید dependencies نہیں ہیں)، پھر دونوں results کے ساتھ get_current_user کو call کرتا ہے، اور آخر میں مکمل طور پر resolve شدہ user کے ساتھ route handler کو call کرتا ہے۔ nesting من چاہی گہرائی تک جا سکتی ہے — یہ واقعی ایک directed acyclic graph ہے، کوئی flat list نہیں — اور FastAPI ہر request میں ہر node کو صرف ایک بار resolve کرتا ہے، چاہے متعدد دوسری dependencies کو اس کی ضرورت ہو، یہی وہ caching behavior ہے جس پر نیچے بات ہوگی۔

Sync اور Async dependencies آسانی سے mix ہو سکتی ہیں

کسی dependency کا def یا async def ہونا اس route handler سے آزاد ہے جو اسے consume کر رہا ہے۔ FastAPI async def dependencies کو براہِ راست event loop پر resolve کرتا ہے اور def dependencies کو threadpool میں dispatch کرتا ہے، بالکل وہی تقسیم جس کی وضاحت route handlers کے لیے Why Your FastAPI Endpoint Blocks the Event Loop میں کی گئی ہے۔ ایک عام اور درست pattern یہ ہے کہ async def route handler کے ساتھ ایک def dependency ہو جو واقعی کچھ synchronous کام کرتی ہو — مثلاً ہلکی CPU config lookup — اور FastAPI اس ایک dependency کے لیے thread dispatch خود بخود handle کر لیتا ہے، آپ کو پورے chain میں signatures match کرنے کی ضرورت نہیں پڑتی۔ بس ایک چیز پر نظر رکھیں: blocking call والا وہی trap۔ def dependency محفوظ ہے کیونکہ وہ threadpooled ہوتی ہے، مگر اگر async def dependency کسی blocking چیز کو await کیے بغیر call کرے تو وہ loop کو بالکل ویسے ہی block کرے گی جیسے کوئی route handler کرتا، کیونکہ dependencies انہی execution rules کی پیروی کرتی ہیں جن handlers میں وہ inject ہوتی ہیں۔

وہ dependencies جو براہِ راست APIRouter پر declare کی جائیں (dependencies=[Depends(...)] کے ذریعے) یا خود app پر لگائی جائیں، ان کے تحت آنے والے ہر route کے لیے چلتی ہیں، بغیر اس کے کہ وہ function parameter کے طور پر ظاہر ہوں — یہ cross-cutting checks کے لیے مفید ہے، جیسے “اس پورے router کے لیے active subscription ضروری ہے”، جہاں dependency کی return value درحقیقت handler کو درکار نہیں ہوتی۔

Yield-based dependencies اور teardown

وہ dependency جسے cleanup درکار ہو — جیسے database session بند کرنا، lock release کرنا، transaction commit یا rollback کرنا — return کے بجائے yield استعمال کرتی ہے۔ yield کے بعد کا code response generate ہونے کے بعد چلتا ہے، اور اہم بات یہ ہے کہ یہ تب بھی چلتا ہے جب route handler exception raise کرے:

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

یہی وہ mechanism ہے جو “session per request” کو قابلِ اعتماد انداز میں درست بناتا ہے — teardown ایسی چیز نہیں رہتی جسے ہر route لکھنے والے کو یاد رکھنا پڑے، بلکہ dependency خود اسے guarantee کرتی ہے، چاہے handler کیسے بھی exit کرے۔ یہ pattern sub-dependencies پر بھی لاگو ہوتا ہے: اگر get_db خود get_current_user کی dependency ہو، تو اس کا teardown تب بھی ان تمام چیزوں کے ختم ہونے کے بعد چلے گا جو اس پر depend کرتی تھیں، اور وہ بھی resolution کی الٹی ترتیب میں — یہ آزاد cleanups کی flat list سے زیادہ try/finally stack سے مشابہ ہے۔

یہی عین pattern اس session-scoping approach کی بنیاد ہے جس کی تفصیل Async SQLAlchemy Sessions in FastAPI, Done Right میں دی گئی ہے — اگر یہاں yield-based teardown درست کر لی جائے تو اس post میں بیان کیے گئے session lifecycle کے زیادہ تر مسائل ابتدا ہی میں پیدا نہیں ہوتے۔

ایک request کے اندر dependency caching

Default طور پر FastAPI ایک dependency کے result کو ایک single request کی lifetime تک cache کرتا ہے — اگر دو مختلف dependencies دونوں Depends(get_db) declare کریں، تو FastAPI get_db کو صرف ایک بار call کرتا ہے اور دونوں callers کو وہی ایک session object دیتا ہے، دو الگ نہیں:

async def get_db():
    print("get_db called")
    ...

def dep_a(db=Depends(get_db)): ...
def dep_b(db=Depends(get_db)): ...

@app.get("/example")
async def example(a=Depends(dep_a), b=Depends(dep_b)):
    # "get_db called" prints exactly once, not twice
    ...

یہ خاص طور پر database sessions کے لیے انتہائی اہم ہے — اس caching کے بغیر dep_a اور dep_b خاموشی سے ایک ہی request کے اندر دو مختلف sessions پر کام کرتے، اور یہ “میرا write نظر کیوں نہیں آ رہا” جیسے subtle bugs کا ذریعہ بنتا، خاص طور پر جب ایک dependency لکھ رہی ہو اور دوسری اسی logical request کے اندر پڑھ رہی ہو۔

یہ caching request تک محدود ہوتی ہے، global نہیں، اور نایاب صورت میں جہاں آپ کو واقعی ایک ہی request میں fresh instance چاہیے ہو وہاں Depends(get_db, use_cache=False) کے ذریعے اسے فی dependency disable کیا جا سکتا ہے۔ یہ جاننا بھی مفید ہے کہ caching key خود callable ہوتی ہے — دو Depends(get_db) calls ایک cache entry share کرتی ہیں کیونکہ وہ ایک ہی function object کو reference کرتی ہیں، مگر دو مختلف functions جو اندرونی طور پر ایک ہی کام کرتے ہوں، cache entry share نہیں کرتے، چاہے ان کی logic ایک جیسی ہی کیوں نہ ہو۔

Shared dependencies درست طریقے سے

وہ dependencies جو متعدد domains میں استعمال ہوتی ہیں، ان کے لیے قدرتی جگہ ایک shared core/dependencies.py ہے، جیسا کہ FastAPI Project Structure That Survives Growth میں بیان کیا گیا ہے — get_db, get_current_user، اور کوئی بھی cross-cutting permission check وہیں ہونی چاہیے، جہاں سے انہیں نیچے کی طرف ہر domain router میں import کیا جائے؛ ہر router میں duplicate نہیں کیا جائے اور domains کے درمیان sideways import بھی نہ کیا جائے۔

# app/core/dependencies.py
from fastapi import Depends, HTTPException, status

async def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_db)):
    user = await db.get_user_by_token(token)
    if user is None:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED)
    return user

async def require_admin(user=Depends(get_current_user)):
    if not user.is_admin:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return user

دیکھیں کہ require_admin، get_current_user پر depend کرتا ہے، token check کو دوبارہ implement نہیں کرتا — یہ composition ہے، اور حقیقی dependency graph کا اصل فائدہ بھی یہی ہے: permission logic، identity logic پر build ہوتی ہے بجائے اس کے کہ اسے duplicate کرے، اور per-request caching کی وجہ سے اگر ایک ہی route میں require_admin اور get_current_user دونوں استعمال ہوں تو user پھر بھی صرف ایک بار resolve ہوتا ہے۔

Anti-pattern: ایک endpoint کو دوسرے سے call کرنا

جب کسی نئے endpoint کو “تقریباً وہی چیز چاہیے جو کسی دوسرے endpoint میں ہے” تو ایک پُرکشش shortcut یہ لگتا ہے کہ اسی endpoint function کو براہِ راست import اور call کر لیا جائے:

# Anti-pattern — do not do this
@app.get("/orders/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@app.get("/orders/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await get_order(id, user)  # calling another route handler directly
    return summarize(order)

یہ بے ضرر لگتا ہے — اور کئی cases میں واقعی درست چل بھی جاتا ہے — مگر یہ کئی ٹھوس طریقوں سے model کو توڑ دیتا ہے۔ call کی گئی function کے Depends parameters FastAPI کے graph کے ذریعے دوبارہ resolve نہیں ہوتے؛ آپ صرف ایک Python function call کر رہے ہوتے ہیں اور arguments کو ہاتھ سے آگے بڑھا رہے ہوتے ہیں، لہٰذا dependency caching، error handling، یا yield-based teardown جنہیں FastAPI عام طور پر coordinate کرتا ہے، اس اندرونی call کے لیے مکمل طور پر bypass ہو جاتے ہیں۔ اگر get_order ایک HTTPException raise کرے، تو وہ get_order_summary کے body میں raw exception کے طور پر propagate ہوتی ہے، بجائے اس کے کہ response میں convert ہو جیسے اس وقت ہوتی اگر client واقعی /orders/{id} hit کرتا — یعنی دونوں code paths ایک ہی error کو مختلف انداز میں handle کرتے ہیں، صرف اس بنیاد پر کہ آپ کس route سے آئے۔ اور یہ خاموشی سے دو endpoints کے HTTP-layer signatures کو آپس میں جوڑ دیتا ہے، چنانچہ get_order کے اپنے route کے لیے parameters بدلنا اب get_order_summary کو بھی توڑ سکتا ہے، حالانکہ اس کا routing سے براہِ راست تعلق نہیں۔

اس کا حل وہ layering ہے جسے Depends دراصل support کرنے کے لیے بنایا گیا ہے: shared logic کو ایک service function میں نکالیں جو سرے سے route handler ہی نہ ہو، اور پھر دونوں routes اسی کو depend کریں یا call کریں۔

# app/orders/service.py
async def fetch_order(order_id: int, user) -> Order:
    order = await db.get_order(order_id)
    if order.owner_id != user.id:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return order

# app/orders/router.py
@router.get("/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@router.get("/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await fetch_order(id, user)
    return summarize(order)

اب دونوں routes حقیقی logic share کرتے ہیں، بغیر اس کے کہ ان میں سے کوئی دوسرے کے HTTP-layer contract پر depend کرے، اور fetch_order کو کسی request کے بغیر بھی نہایت آسانی سے unit-test کیا جا سکتا ہے۔

Class-based dependencies

ایسی dependency کے لیے جسے configuration درکار ہو — جیسے pagination limit یا مطلوبہ permission scope — __call__ والی class آپ کو ایک parameterized، reusable dependency دیتی ہے، بجائے تقریباً ایک جیسی functions کے پورے خاندان کے:

class RequirePermission:
    def __init__(self, scope: str):
        self.scope = scope

    def __call__(self, user=Depends(get_current_user)):
        if self.scope not in user.scopes:
            raise HTTPException(status.HTTP_403_FORBIDDEN)
        return user

require_orders_write = RequirePermission("orders:write")

@router.post("/orders", dependencies=[Depends(require_orders_write)])
async def create_order(payload: OrderCreate):
    ...

یہ الگ الگ functions جیسے require_orders_write_permission, require_payments_read_permission وغیرہ لکھنے کے مقابلے میں کہیں بہتر scale کرتا ہے — parameterization constructor میں رہتی ہے، اور FastAPI کو نظر آنے والی shape پوری app میں ہر permission check کے لیے ایک ہی، consistent callable pattern رہتی ہے۔

Class-based dependencies __call__ کو generator کے طور پر بھی استعمال کر سکتی ہیں، یوں parameterization کو yield-based teardown کے ساتھ جوڑا جا سکتا ہے — مثلاً ایسے rate limiter کے لیے مفید جو request ختم ہونے کے بعد token release کرنا چاہتا ہو:

class RateLimiter:
    def __init__(self, requests_per_minute: int):
        self.limit = requests_per_minute

    async def __call__(self, user=Depends(get_current_user)):
        token = await acquire_slot(user.id, self.limit)
        try:
            yield
        finally:
            await release_slot(token)

throttle_reports = RateLimiter(requests_per_minute=5)

@router.get("/reports", dependencies=[Depends(throttle_reports)])
async def get_reports():
    ...

ہر instance ایک بار، import time پر بنتی ہے، اور پھر ہر request میں reuse ہوتی ہے — constructor arguments dependency کو configure کرتے ہیں، جبکہ __call__ کو اب بھی ہر دوسری dependency کی طرح مکمل per-request resolution، caching، اور teardown behavior حاصل رہتا ہے۔

dependency_overrides کے ساتھ testing

حقیقی dependencies بنانے کا دوسرا بڑا فائدہ، بجائے logic کو handlers میں inline کرنے کے، app.dependency_overrides ہے — ایک dict جسے FastAPI کسی بھی dependency کو resolve کرنے سے پہلے check کرتا ہے، جس سے tests get_db کو test database سے، یا get_current_user کو کسی fixed test user سے swap کر سکتے ہیں، اور route code کو ہاتھ بھی نہیں لگانا پڑتا:

# tests/conftest.py
from app.main import app
from app.core.dependencies import get_db, get_current_user

async def override_get_db():
    async with TestSessionLocal() as session:
        yield session

def override_get_current_user():
    return User(id=1, email="test@example.com", is_admin=False)

app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
# tests/orders/test_router.py
async def test_create_order(client):
    response = await client.post("/orders", json={"item": "widget", "qty": 2})
    assert response.status_code == 200

نہ mocking library کی ضرورت، نہ internals پر monkeypatching، نہ کسی حقیقی Postgres instance یا حقیقی OAuth flow کو صرف اس لیے چلانے کی ضرورت کہ آپ ایک ایسا endpoint hit کرنا چاہتے ہیں جسے login چاہیے۔ یہی وہ بنیادی وجہ ہے کہ اس post میں پہلے بیان کیا گیا anti-pattern صرف code cleanliness سے کہیں زیادہ اہم ہے: ایسا route جو کسی دوسرے route function کو براہِ راست call کرتا ہو، اسے اس طریقے سے exercise نہیں کیا جا سکتا، کیونکہ اندرونی call کبھی FastAPI کی resolution machinery سے گزرتی ہی نہیں — dependency_overrides کے پاس intercept کرنے کو کچھ نہیں ہوتا۔ اس کے برعکس، وہ route جو Depends کے ذریعے کسی حقیقی service function پر depend کرتا ہو، مکمل طور پر تنہائی میں test کیا جا سکتا ہے، اور اس کے graph کی ہر dependency کو الگ الگ swap کیا جا سکتا ہے۔

Overrides بھی ویسے ہی compose ہوتی ہیں جیسے dependencies خود compose ہوتی ہیں — get_db کو override کرنے سے وہ ہر اس dependency کے لیے override ہو جاتی ہے جو transitively اس پر depend کرتی ہے، بشمول get_current_user، اور اس کے لیے ہر ایک کی الگ override درکار نہیں ہوتی۔ بس یاد رکھیں کہ test modules کے درمیان overrides clear کر دیں (app.dependency_overrides.clear() کو fixture teardown میں) — ایک test file سے بچ جانے والی override اگر اگلی میں خاموشی سے behavior بدل دے تو یہ الجھن پیدا کرنے والی، order-dependent test failures کا عام سبب بنتی ہے۔

عام pitfalls

غلطی: business logic کو service کے بجائے براہِ راست dependency میں رکھ دینا۔ ایسا Depends function جو query کرے، transform کرے، اور مکمل process شدہ business object return کرے، اس logic کو ہر اس چیز سے پوشیدہ بنا دیتا ہے جو FastAPI route نہیں ہے۔ حل: dependencies کو request-scoped setup (auth, sessions, pagination) تک محدود رکھیں اور اصل business logic کو service functions کے سپرد کریں۔

غلطی: یہ سمجھ لینا کہ ہر Depends(x) call ایک ہی cache entry share کرتی ہے، چاہے x کو جیسے بھی reference کیا گیا ہو۔ دو functions جو ایک ہی کام کرتے ہوں مگر ایک ہی object نہ ہوں، ایک ہی cache slot share نہیں کرتے۔ حل: جہاں جہاں آپ caching چاہتے ہیں وہاں بالکل اسی callable کو import کریں — مختلف جگہوں پر بظاہر ایک جیسی dependencies دوبارہ define نہ کریں۔

غلطی: یہ بھول جانا کہ yield-based teardown اس وقت تک نہیں چلتی جب تک response مکمل طور پر generate نہ ہو جائے۔ yield کے بعد کا code handler کے return کرنے کے بعد چلتا ہے، یعنی یہ اس chain کی ہر اس dependency کے بعد بھی چلتا ہے جو اس پر اوپر کی سطح پر depend کرتی ہو۔ حل: خاص طور پر nested dependencies کے ساتھ، اصل dependency graph کو trace کیے بغیر teardown ordering فرض نہ کریں۔

غلطی: ایک route handler function کو دوسرے route سے براہِ راست call کرنا۔ جیسا کہ اوپر بیان ہوا، یہ caching، exception handling، اور teardown guarantees کو bypass کرتا ہے۔ حل: shared logic کو ایک plain service function میں نکالیں جس کی ملکیت کسی route کے پاس نہ ہو۔

غلطی: dependencies=[...] کو router-level پر ان چیزوں کے لیے حد سے زیادہ استعمال کرنا جن کی return value دراصل handler کو چاہیے ہوتی ہے۔ اگر handler کو resolved user object چاہیے، تو اسے router-level dependencies میں چھپا دینے سے value واپس لینے کے لیے دوسری، غیر ضروری Depends(get_current_user) parameter لکھنی پڑتی ہے۔ حل: router-level dependencies صرف انہی checks کے لیے استعمال کریں جن کی return value کسی کو درکار نہ ہو۔

Production best practices

  • Dependencies کو request-scoped اور side-effect-light رکھیں۔ Setup، auth checks، اور session management یہاں مناسب ہیں؛ multi-step business logic نہیں۔
  • Permission checks کو duplicate کرنے کے بجائے compose کریں۔ require_admin کا get_current_user پر depend کرنا، ہر permission level کے لیے token check دوبارہ implement کرنے سے سستا اور زیادہ consistent ہے۔
  • ہمیشہ yield کو try/finally کے ساتھ pair کریں تاکہ teardown اس وقت بھی چلے جب handler exception raise کرے — صرف happy path پر انحصار نہ کریں۔
  • کبھی ایک route handler کو دوسرے سے call نہ کریں۔ اگر دو endpoints کو ایک ہی logic چاہیے، تو وہ logic کسی service function میں ہونی چاہیے، کسی handler میں نہیں۔
  • جب ایک ہی check کی دو یا تین سے زیادہ parameterized variants ہو جائیں تو class-based dependencies استعمال کریں۔ اس سے permission surface area consistent اور audit کرنے میں آسان رہتی ہے۔

اختتامیہ

Depends ایک حقیقی dependency graph ہے جس میں caching، ordering، اور lifecycle guarantees موجود ہیں — یہ محض ایک decorator نہیں جو اتفاقاً current user لے آتا ہو۔ جب اسے سوچ سمجھ کر استعمال کیا جائے تو یہی وہ چیز ہے جو auth، sessions، اور cross-cutting checks کو بڑھتے ہوئے routers کے مجموعے میں consistent رکھتی ہے، بغیر اس کے کہ ہر endpoint پر logic duplicate کرنی پڑے۔ لیکن اگر اسے لاپروائی سے استعمال کیا جائے — خاص طور پر route handler کو ایک عام function سمجھ کر کہیں اور سے call کیا جائے — تو یہ خاموشی سے انہی guarantees کو توڑ دیتا ہے جن کی وجہ سے اسے استعمال کرنا ابتدا میں فائدہ مند تھا۔

کیا آپ کی dependencies صرف request-scoped setup کر رہی ہیں، یا ان میں سے کچھ خاموشی سے وہ جگہ بن چکی ہیں جہاں آپ کی business logic رہتی ہے؟ اگر آپ کو یقین نہیں، تو dependency_overrides اس کا پتہ لگانے کا تیز طریقہ ہے — ایسی dependency جسے آپ test میں صاف طور پر swap نہ کر سکیں، عموماً وہی ہوتی ہے جس نے اپنی اصل ذمہ داری سے زیادہ بوجھ اٹھا لیا ہوتا ہے۔

مزید مضامین