- Define route handlers as `async def homepage(request): return JSONResponse({'hello': 'world'})` and register with `Route('/path', endpoint, methods=['GET'])`.
- Use `request.path_params['id']` for URL params, `await request.json()` for body, `request.query_params['page']` for query strings.
- Return `JSONResponse`, `HTMLResponse`, or `StreamingResponse` — never return raw dicts or strings.
- Mount sub-applications with `Mount('/api', app=api_routes)` for modular organization.
- Use `request.state` to pass data between middleware and route handlers.
- Define async route handlers and register in `routes` list: `routes = [Route('/users/{id}', get_user, methods=['GET']), Route('/users', create_user, methods=['POST'])]`.
- Parse path params with `request.path_params['id']`, query params with `request.query_params.get('page', '1')`, and JSON body with `data = await request.json()` — always validate after parsing.
- Return explicit response types: `JSONResponse({'id': user.id}, status_code=201)` for success, `JSONResponse({'error': 'not found'}, status_code=404)` for errors — never rely on implicit serialization.
- Write middleware as ASGI callables: `class AuthMiddleware: async def __call__(self, scope, receive, send): if scope['type'] == 'http': ...` and add to `Middleware(AuthMiddleware)`.
- Mount sub-applications: `Mount('/api/v1', routes=[Route('/users', ...)])` — Starlette's `Router` and `Mount` compose cleanly for versioned APIs.
- Use `BackgroundTask` for work that should happen after the response: `return JSONResponse({'status': 'ok'}, background=BackgroundTask(send_notification, user_id))`.