[{"data":1,"prerenderedAt":14085},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-2":4,"blog-paginated-cats":13439},640,[5,277,502,702,1347,2168,2492,3483,5860,7739,7992,8590,10117,10376,10789],{"id":6,"title":7,"author":8,"body":11,"category":256,"date":257,"description":258,"extension":259,"featured":260,"image":261,"keywords":262,"meta":265,"navigation":266,"path":267,"readTime":268,"seo":269,"stem":270,"tags":271,"__hash__":276},"blog/blog/ai-for-small-business.md","AI for Small Business: Where to Start Without Wasting Money",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":238},"minimark",[14,19,23,26,29,32,36,39,42,45,47,51,56,59,62,65,69,72,75,78,82,85,88,91,95,98,101,105,108,111,113,117,120,127,133,139,142,144,148,154,160,166,172,174,178,181,184,187,190,193,204,206,210],[15,16,18],"h2",{"id":17},"the-small-business-ai-problem","The Small Business AI Problem",[20,21,22],"p",{},"Small business owners face a particular challenge with AI adoption. The technology is moving fast and the hype is loud. Every week there's a new tool claiming to transform your operations. Sales pitches promise dramatic productivity improvements and cost savings. And there's real FOMO — competitors may be using AI to get ahead.",[20,24,25],{},"The result is often one of two failure modes: spending money on AI tools that don't deliver meaningful value (because they were bought based on hype rather than fit), or paralysis (because the options are overwhelming and it's unclear where to start).",[20,27,28],{},"I work with small and mid-size businesses on their software and technology strategy. Here's the framework I use to cut through the noise and identify AI investments that actually pay off.",[30,31],"hr",{},[15,33,35],{"id":34},"the-right-starting-question","The Right Starting Question",[20,37,38],{},"Don't start with \"what AI tools should we use?\" Start with: \"what are the most time-consuming, repetitive tasks in our business that don't require genuine human judgment?\"",[20,40,41],{},"Those are the first candidates for AI automation. Not because AI can't help with more complex work — it can — but because the ROI on automating repetitive tasks is immediate and measurable. Every hour of repetitive work that gets automated is a direct, calculable cost reduction or capacity increase.",[20,43,44],{},"The inventory I recommend every small business owner do before talking to any AI vendor or consultant: list the tasks that eat your team's time each week. For each task, ask: Is this repetitive? Is it rule-based or pattern-based? Is a human required because of judgment or because nobody has automated it? The last category is your opportunity list.",[30,46],{},[15,48,50],{"id":49},"ai-applications-with-clear-small-business-roi","AI Applications With Clear Small Business ROI",[52,53,55],"h3",{"id":54},"customer-communication-and-response","Customer Communication and Response",[20,57,58],{},"For businesses that handle significant customer communication volume — emails, chat, support requests — AI drafting assistance significantly reduces the time cost per interaction without requiring a dedicated AI platform.",[20,60,61],{},"The most accessible starting point: AI writing assistance (built into many email tools now) that drafts responses based on the context of incoming messages. A customer asks about your return policy; the AI drafts a response that you review and send. You spend 30 seconds instead of 3 minutes.",[20,63,64],{},"At higher volume or complexity, a small business can build or buy a more dedicated customer support AI. But even at the basic level of AI drafting assistance, the time savings for businesses with active customer communication are meaningful.",[52,66,68],{"id":67},"appointment-scheduling-and-booking","Appointment Scheduling and Booking",[20,70,71],{},"If your business relies on appointments — professional services, consulting, healthcare-adjacent services, personal services — AI scheduling automation that handles the back-and-forth of finding times, sending confirmations, and managing cancellations is one of the clearest small business AI wins.",[20,73,74],{},"The value is not just time saved — it's also availability. AI scheduling systems work 24/7. Customers can book at 11pm. The business captures appointments that would have been missed because the staff wasn't available to respond immediately.",[20,76,77],{},"Tools for this range from sophisticated scheduling AI to simple Calendly integrations. The technology is mature and accessible.",[52,79,81],{"id":80},"content-and-marketing","Content and Marketing",[20,83,84],{},"Small businesses typically underinvest in content marketing because content creation is time-consuming relative to a small team's capacity. AI writing tools dramatically reduce the time cost of producing blog content, social media posts, email newsletters, and marketing copy.",[20,86,87],{},"The appropriate expectation: AI-generated marketing content requires human review and often refinement. It's a starting point, not a finished product. But \"edit a 600-word AI draft\" takes 20 minutes. \"Write a 600-word blog post from scratch\" takes 90 minutes. For a small business owner wearing many hats, that difference matters.",[20,89,90],{},"The caveat: don't publish AI content without review. AI-generated marketing copy can be generic, occasionally inaccurate, and sometimes oddly phrased in ways that undermine your brand voice. Human judgment in the review step is non-negotiable.",[52,92,94],{"id":93},"data-entry-and-document-processing","Data Entry and Document Processing",[20,96,97],{},"Many small businesses spend significant staff time on data entry — entering information from paper forms, emails, invoices, and documents into systems. AI document processing (extracting structured data from unstructured documents) automates this.",[20,99,100],{},"Invoice processing, expense classification, contract data extraction, form digitization — these are high-value automation targets because the work is mechanical, time-consuming, and error-prone when done manually. Modern AI document processing tools achieve accuracy rates that make automation viable for most small business document processing workflows.",[52,102,104],{"id":103},"internal-knowledge-and-faq","Internal Knowledge and FAQ",[20,106,107],{},"Businesses with multiple employees answer the same internal questions repeatedly. AI-powered internal knowledge bases let employees ask questions (\"what's our refund policy for clients who cancel after 30 days?\") and get accurate answers instantly, rather than asking a manager or finding the right document.",[20,109,110],{},"The time savings are modest per interaction but significant in aggregate. More importantly, it improves consistency — everyone gets the same accurate answer rather than whoever is available's recollection.",[30,112],{},[15,114,116],{"id":115},"the-tools-to-evaluate-first","The Tools to Evaluate First",[20,118,119],{},"For small businesses, the right starting point is usually tools you're already paying for that have added AI capabilities, not new standalone AI tools:",[20,121,122,126],{},[123,124,125],"strong",{},"Microsoft 365 Copilot / Google Workspace AI",": If you're paying for these platforms (and most small businesses are), the AI features built into them — email drafting, document summarization, meeting summaries, spreadsheet assistance — are already included or available at a modest add-on cost.",[20,128,129,132],{},[123,130,131],{},"CRM AI features",": Most major CRM platforms (HubSpot, Salesforce, even smaller SMB-focused options) have added AI features for lead scoring, email drafting, and activity summarization. These are worth evaluating before building custom solutions.",[20,134,135,138],{},[123,136,137],{},"Accounting software AI",": Automation for expense categorization, invoice matching, and financial reporting summaries is now available in most major small business accounting platforms.",[20,140,141],{},"The pattern: evaluate AI capabilities in your existing tools before adding new tools. Adding new tools adds integration complexity and learning overhead. Getting more value from existing tools is typically lower friction.",[30,143],{},[15,145,147],{"id":146},"what-to-avoid","What to Avoid",[20,149,150,153],{},[123,151,152],{},"AI tools marketed primarily as \"AI-powered\"",": If the main selling point is that a tool uses AI, be skeptical. The selling point should be the business problem it solves. \"AI-powered\" is not a benefit; it's an implementation detail.",[20,155,156,159],{},[123,157,158],{},"Custom AI development before basics are in place",": Unless you have a specific problem that packaged tools genuinely can't solve, custom AI development is not where a small business should start. Get value from existing tools first, then build custom when you have a clear, specific gap.",[20,161,162,165],{},[123,163,164],{},"Automating broken processes",": Automating a process that works poorly just produces broken automation faster. Fix the process first. AI automation is most valuable when you're automating something that works well and just needs to be faster or cheaper.",[20,167,168,171],{},[123,169,170],{},"Replacing humans without redesigning workflow",": AI automation that simply removes a human from a task without redesigning the workflow around it often misses most of the potential value and creates gaps in capability. Think about the full workflow, not just the task.",[30,173],{},[15,175,177],{"id":176},"a-practical-starting-point","A Practical Starting Point",[20,179,180],{},"If I were advising a small business owner with no current AI investment, here's what I'd recommend starting with:",[20,182,183],{},"Week 1: Use your existing productivity tools' AI features for two weeks — email drafting, document summarization, whatever's available. Get comfortable with AI assistance for routine communication tasks before adding anything new.",[20,185,186],{},"Month 1: Identify your highest-volume repetitive task and find a tool that specifically addresses it. Test it for 30 days with measurable metrics.",[20,188,189],{},"Quarter 1: Based on what you've learned, make a decision about whether to expand AI usage in that area, move to a different area, or maintain current level.",[20,191,192],{},"This iterative approach avoids the trap of committing significant investment to AI tools before understanding what delivers value for your specific business. The best AI investment for your business is specific to your business — it depends on your workflows, your team, your customer interactions, and your growth constraints.",[20,194,195,196,203],{},"If you want help identifying where AI can deliver real value for your specific business rather than generic advice, ",[197,198,202],"a",{"href":199,"rel":200},"https://calendly.com/jamesrossjr",[201],"nofollow","let's have a conversation at Calendly",". I'll help you see the opportunities clearly and avoid the wasted investments.",[30,205],{},[15,207,209],{"id":208},"keep-reading","Keep Reading",[211,212,213,220,226,232],"ul",{},[214,215,216],"li",{},[197,217,219],{"href":218},"/blog/ai-data-analysis-business","AI for Business Data Analysis: Moving Beyond Spreadsheets",[214,221,222],{},[197,223,225],{"href":224},"/blog/automated-testing-with-ai","Automated Testing With AI: Faster Coverage, Fewer Blind Spots",[214,227,228],{},[197,229,231],{"href":230},"/blog/ai-software-development-trends-2026","AI Software Development Trends for 2026: A Practitioner's View",[214,233,234],{},[197,235,237],{"href":236},"/blog/ai-for-devops","AI for DevOps: Smarter Deployments, Faster Incident Response",{"title":239,"searchDepth":240,"depth":240,"links":241},"",3,[242,244,245,252,253,254,255],{"id":17,"depth":243,"text":18},2,{"id":34,"depth":243,"text":35},{"id":49,"depth":243,"text":50,"children":246},[247,248,249,250,251],{"id":54,"depth":240,"text":55},{"id":67,"depth":240,"text":68},{"id":80,"depth":240,"text":81},{"id":93,"depth":240,"text":94},{"id":103,"depth":240,"text":104},{"id":115,"depth":243,"text":116},{"id":146,"depth":243,"text":147},{"id":176,"depth":243,"text":177},{"id":208,"depth":243,"text":209},"AI","2026-03-03","A practical, no-hype guide to AI adoption for small businesses — identifying the applications that deliver real ROI, avoiding the common traps, and making smart decisions about where to start.","md",false,null,[263,264],"AI for small business","AI software development services",{},true,"/blog/ai-for-small-business",8,{"title":7,"description":258},"blog/ai-for-small-business",[272,256,273,274,275],"Small Business","Business Strategy","ROI","Automation","U0XpvxIrD_3BTvb92wZIxWRVQUgkL0Dyp_LyBUp_Z0w",{"id":278,"title":279,"author":280,"body":281,"category":256,"date":257,"description":488,"extension":259,"featured":260,"image":261,"keywords":489,"meta":492,"navigation":266,"path":493,"readTime":268,"seo":494,"stem":495,"tags":496,"__hash__":501},"blog/blog/ai-powered-code-review.md","AI-Powered Code Review: What Works, What Doesn't, and How I Use It",{"name":9,"bio":10},{"type":12,"value":282,"toc":469},[283,287,290,293,295,299,303,306,309,312,316,319,322,326,329,332,336,339,342,344,348,352,355,358,361,365,368,371,375,378,381,385,388,390,394,397,400,403,406,409,411,415,418,421,423,427,430,433,441,443,445],[15,284,286],{"id":285},"the-honest-state-of-ai-code-review","The Honest State of AI Code Review",[20,288,289],{},"AI code review tools have gotten very good at a specific set of things. They've gotten those things right enough that using them is clearly better than not using them. But they haven't gotten good at everything, and the gaps matter — especially because there's a temptation to over-trust automated review and under-invest in the human review it's supposed to complement.",[20,291,292],{},"I use AI code review tools in my own practice every day. I've also watched teams adopt them poorly — either dismissing every AI comment as noise or, worse, rubber-stamping reviews because the AI passed them. Both failure modes are real. Let me tell you what I actually see these tools do well and where they consistently fall short.",[30,294],{},[15,296,298],{"id":297},"what-ai-code-review-gets-right","What AI Code Review Gets Right",[52,300,302],{"id":301},"pattern-level-bug-detection","Pattern-Level Bug Detection",[20,304,305],{},"AI code review is excellent at finding bugs that match known patterns. Off-by-one errors in loops. Missing null checks on values that could be undefined. Race conditions in async code where awaits are missing or misplaced. SQL injection vectors from unsanitized inputs. Common XSS vulnerabilities in template rendering.",[20,307,308],{},"These are the bugs that a tired human reviewer misses because they're reading for understanding rather than scrutinizing every line. AI tools are tireless and they've been trained on millions of examples of these patterns going wrong. In my experience, they catch a meaningful percentage of real bugs in every review — enough to justify the workflow overhead many times over.",[20,310,311],{},"The key is that these are pattern-matching tasks, and language models are exceptional at pattern matching. They've seen the bug you're about to ship many, many times.",[52,313,315],{"id":314},"security-vulnerability-identification","Security Vulnerability Identification",[20,317,318],{},"Related to bug detection: AI tools are good at security vulnerability identification at the code level. Hardcoded secrets. Overly permissive CORS configurations. Missing authentication checks on routes. Insecure cryptographic choices. Injection vulnerabilities.",[20,320,321],{},"I run AI code review as an early pass on every security-sensitive component. It doesn't replace a thorough security audit — the AI won't catch architectural vulnerabilities or business logic flaws. But it reliably catches the mechanical security mistakes that account for a large share of real-world vulnerabilities.",[52,323,325],{"id":324},"code-style-and-consistency","Code Style and Consistency",[20,327,328],{},"AI review is reliable at enforcing style consistency — naming conventions, import ordering, file structure patterns, consistent use of language features. This is the kind of review feedback that's valuable but tedious for human reviewers to provide consistently. Automated tools do it better.",[20,330,331],{},"More usefully, AI review can flag inconsistencies specific to your codebase's conventions, not just generic style rules. If you use certain naming patterns throughout your project and a new contributor breaks them, a good AI review tool will catch it.",[52,333,335],{"id":334},"documentation-and-test-coverage-gaps","Documentation and Test Coverage Gaps",[20,337,338],{},"AI review tools are reasonably good at flagging undocumented public APIs and missing test coverage for new functionality. They won't write the docs or tests for you (well, modern tools increasingly will generate them), but they'll flag the gaps reliably.",[20,340,341],{},"This is useful for maintaining quality standards across a team where discipline around documentation and testing varies by contributor.",[30,343],{},[15,345,347],{"id":346},"where-ai-code-review-falls-short","Where AI Code Review Falls Short",[52,349,351],{"id":350},"architectural-and-design-decisions","Architectural and Design Decisions",[20,353,354],{},"Here's where human review is irreplaceable: understanding whether a change is architecturally sound. Is this the right abstraction? Does this new service create problematic coupling with existing services? Is this data model going to scale? Is this approach consistent with the design decisions made three months ago in ADR-0047?",[20,356,357],{},"AI tools don't have this context. They might flag that your new code has high cyclomatic complexity (a legitimate observation) but they can't tell you whether the complexity is acceptable given the business requirements, or whether the real problem is that you're solving the wrong problem in the first place.",[20,359,360],{},"Architectural review requires human judgment, context about the system's history and direction, and understanding of constraints the AI tool has no access to.",[52,362,364],{"id":363},"business-logic-correctness","Business Logic Correctness",[20,366,367],{},"An AI tool can tell you that your order processing function handles the null case incorrectly. It cannot tell you that the tax calculation logic is wrong for the specific business rules of your client in Texas. Business logic correctness requires domain knowledge that isn't available to the model.",[20,369,370],{},"I've seen teams get a false sense of security from AI review passes on code with subtle business logic bugs. The bugs weren't detectable without understanding the domain requirements — the code was internally consistent and syntactically correct. It just implemented the wrong rules.",[52,372,374],{"id":373},"test-quality-assessment","Test Quality Assessment",[20,376,377],{},"AI review can tell you that tests exist. It cannot reliably tell you that tests are good. A test suite that covers 90% of code paths but tests only happy paths, makes overly broad assertions, and doesn't cover the edge cases that actually fail in production will pass AI review with flying colors.",[20,379,380],{},"Test quality assessment requires understanding of what the code is supposed to do and what could go wrong — domain knowledge again.",[52,382,384],{"id":383},"subtle-concurrency-issues","Subtle Concurrency Issues",[20,386,387],{},"AI tools get the obvious concurrency bugs. They miss the subtle ones. Race conditions that only manifest under specific timing conditions. Deadlocks that require specific sequences of operations across multiple services. Starvation issues in complex queue systems. These require the kind of careful, contextual reasoning that current AI code review tools don't reliably provide.",[30,389],{},[15,391,393],{"id":392},"how-i-actually-use-these-tools","How I Actually Use These Tools",[20,395,396],{},"Here's my workflow in practice. AI code review is the first pass on every PR, automated. I use it to catch the mechanical issues — patterns, security, style, obvious bugs. This happens before any human reviewer looks at the code.",[20,398,399],{},"When AI review flags something, I take it seriously. I don't dismiss comments just because they're automated. A flagged security issue is a flagged security issue whether a human or an AI caught it.",[20,401,402],{},"When AI review passes cleanly, I do not reduce my human review rigor. A clean AI review means the mechanical layer is in order. The human review is about architecture, design, business logic, test quality, and the questions that require context.",[20,404,405],{},"I also use AI tools proactively during development, not just at review time. Before opening a PR, I'll run the code I've written through an AI review pass to catch issues I might have introduced. This reduces the feedback cycle — I'd rather catch a bug during development than at review time.",[20,407,408],{},"The specific tools I use change as the ecosystem evolves. Claude Code's built-in code review, GitHub Copilot's review features, and purpose-built tools like CodeRabbit all have different strengths. I don't have one-tool loyalty — I pick based on the project context.",[30,410],{},[15,412,414],{"id":413},"the-review-fatigue-problem","The Review Fatigue Problem",[20,416,417],{},"One failure mode I want to call out specifically: AI code review tools that produce too many comments create review fatigue. When reviewers are conditioned to see 30 AI comments on every PR and most of them are low-value style nitpicks, they start ignoring all of them — including the important ones.",[20,419,420],{},"The solution is configuration discipline. Tune your AI review tools aggressively. Silence the categories that aren't producing signal. Elevate the categories that matter most for your context (security, for example, should never be silenced). A tool that gives you five high-signal comments per PR is far more valuable than one that gives you thirty comments of varying quality.",[30,422],{},[15,424,426],{"id":425},"my-recommendation","My Recommendation",[20,428,429],{},"Use AI code review. The ROI is clearly positive when used correctly. Don't use it as a replacement for thoughtful human review — use it as the first pass that clears the mechanical issues so human reviewers can focus on what they're uniquely qualified to assess.",[20,431,432],{},"The teams getting value from AI code review are the ones who've integrated it into a disciplined review process. The teams getting false confidence are the ones who've let it replace review discipline rather than complement it.",[20,434,435,436,440],{},"If you're setting up a development workflow that uses AI tools effectively and want a second opinion on your approach, ",[197,437,439],{"href":199,"rel":438},[201],"schedule a conversation at Calendly",". I'm happy to talk through what I've seen work and what I'd avoid.",[30,442],{},[15,444,209],{"id":208},[211,446,447,453,459,463],{},[214,448,449],{},[197,450,452],{"href":451},"/blog/ai-documentation-generation","AI-Generated Documentation: What It Can and Can't Replace",[214,454,455],{},[197,456,458],{"href":457},"/blog/ai-code-generation-tools-compared","AI Code Generation Tools: How I Actually Use Them in Production",[214,460,461],{},[197,462,231],{"href":230},[214,464,465],{},[197,466,468],{"href":467},"/blog/agentic-ai-software-development","Agentic AI Software Development: What It Is and Why It Changes Everything",{"title":239,"searchDepth":240,"depth":240,"links":470},[471,472,478,484,485,486,487],{"id":285,"depth":243,"text":286},{"id":297,"depth":243,"text":298,"children":473},[474,475,476,477],{"id":301,"depth":240,"text":302},{"id":314,"depth":240,"text":315},{"id":324,"depth":240,"text":325},{"id":334,"depth":240,"text":335},{"id":346,"depth":243,"text":347,"children":479},[480,481,482,483],{"id":350,"depth":240,"text":351},{"id":363,"depth":240,"text":364},{"id":373,"depth":240,"text":374},{"id":383,"depth":240,"text":384},{"id":392,"depth":243,"text":393},{"id":413,"depth":243,"text":414},{"id":425,"depth":243,"text":426},{"id":208,"depth":243,"text":209},"An honest breakdown of AI code review tools in 2026 — what they catch reliably, where they miss, and how to integrate them without creating review fatigue or false confidence.",[490,491],"AI code review","ai software development tools",{},"/blog/ai-powered-code-review",{"title":279,"description":488},"blog/ai-powered-code-review",[256,497,498,499,500],"Code Review","Developer Tools","Software Quality","AI Development","mLhVKcXFB_lu9Urg8ZjSh5ptupYv5YASmBOWyYmbX6c",{"id":503,"title":231,"author":504,"body":505,"category":256,"date":257,"description":690,"extension":259,"featured":260,"image":261,"keywords":691,"meta":694,"navigation":266,"path":230,"readTime":268,"seo":695,"stem":696,"tags":697,"__hash__":701},"blog/blog/ai-software-development-trends-2026.md",{"name":9,"bio":10},{"type":12,"value":506,"toc":678},[507,511,514,517,520,522,526,529,532,535,537,541,544,547,550,552,556,559,562,565,567,571,574,577,580,582,586,589,592,595,598,600,604,607,610,613,615,619,622,625,628,630,634,637,640,643,650,652,654],[15,508,510],{"id":509},"what-matters-in-2026-vs-what-just-makes-noise","What Matters in 2026 vs. What Just Makes Noise",[20,512,513],{},"Every year, the trend articles come out. Most of them recycle the same list with updated year numbers. I'm not going to do that. I'm a software architect who builds AI-native applications for businesses in Dallas and remotely, and I want to share what I'm actually seeing in the work — not what's trending on Hacker News or LinkedIn.",[20,515,516],{},"2026 is different from 2025 in ways that matter. The pattern I'm observing: the novelty phase of AI in development is over. Developers who were experimenting are now productionizing. Businesses that were skeptical are now asking how to catch up. And the tools have matured enough that the real gaps — architectural, organizational, and in some cases ethical — are becoming visible.",[20,518,519],{},"Here are the trends I'm watching closely and why they matter for anyone making decisions about software this year.",[30,521],{},[15,523,525],{"id":524},"_1-agentic-development-is-leaving-the-lab","1. Agentic Development Is Leaving the Lab",[20,527,528],{},"In 2024 and most of 2025, AI agents in software development were demos and research. In 2026, they're workflows. Teams are deploying agents that do real things: read a codebase, write a failing test, implement the feature that makes the test pass, open a pull request, and flag it for review. Human in the loop at the gates, automation in between.",[20,530,531],{},"I've been building with Claude Code and the Anthropic SDK in my own practice since early 2025. The shift from \"this is impressive\" to \"I can't imagine building without it\" happened around Q3 2025. What changed wasn't the model capability — it was the tooling around context management and tool use. Agents that can read files, execute code, run tests, and iterate on the results are qualitatively different from chat assistants that help you think.",[20,533,534],{},"What this means practically: if you're planning software architecture in 2026, you need to think about agent-friendliness. Codebases with consistent naming, strong typing, and well-scoped modules are dramatically easier for agents to work in. Spaghetti code that a human developer can navigate by tribal knowledge is a dead end for agentic workflows.",[30,536],{},[15,538,540],{"id":539},"_2-context-windows-are-reshaping-architecture-decisions","2. Context Windows Are Reshaping Architecture Decisions",[20,542,543],{},"A year ago, the limiting factor on LLM usefulness in development was context window size. You couldn't fit an entire codebase in context, so agents had to work blind on much of the system. That constraint shaped the early tool ecosystems — everything was about chunking, summarizing, and retrieval.",[20,545,546],{},"The constraint still exists, but the threshold has shifted dramatically. Working with 200k+ token windows changes what's architecturally possible. Agents can now understand an entire service, its tests, its dependencies, and relevant documentation simultaneously. This doesn't eliminate the need for RAG (Retrieval-Augmented Generation) — it changes when you need it and why.",[20,548,549],{},"The architectural implication: context window capacity is now a factor in LLM selection, and it should be. For agentic development workflows, the ability to hold a large working context in memory changes the quality of output significantly. This is one of the key variables I evaluate when scoping AI integration projects.",[30,551],{},[15,553,555],{"id":554},"_3-the-model-commodity-trap","3. The Model Commodity Trap",[20,557,558],{},"Here's an uncomfortable trend: the underlying models are commoditizing faster than the tooling around them. The gap between the top-tier models (Claude, GPT-4, Gemini) has narrowed enough that for most development tasks, model selection is less important than how you're prompting, what context you're providing, and what infrastructure surrounds the call.",[20,560,561],{},"This has a business implication. Companies that are building a competitive advantage on \"we use the best AI model\" are building on sand. Companies building advantages on proprietary data, fine-tuned domain models, and well-engineered retrieval systems are building something defensible.",[20,563,564],{},"For software architects, this means the AI integration work that matters is not model selection — it's the data layer, the prompt architecture, the evaluation pipelines, and the feedback loops. The model is a commodity component; the system around it is the differentiator.",[30,566],{},[15,568,570],{"id":569},"_4-ai-native-vs-ai-augmented-is-now-a-real-distinction","4. AI-Native vs. AI-Augmented Is Now a Real Distinction",[20,572,573],{},"In 2026, I'm drawing a clear line between two architectural approaches that I see clients conflate constantly. An AI-augmented application adds AI features to an existing architecture — a chatbot bolted onto a CRM, an AI summary added to a document system. An AI-native application is designed from the ground up with AI in the critical path.",[20,575,576],{},"The distinction matters because they have different requirements. AI-native applications need structured AI integration at the data layer, observability into model behavior, fallback logic when models fail, evaluation systems for output quality, and cost management infrastructure. These are not afterthoughts — they're core architecture concerns.",[20,578,579],{},"I'm seeing more clients request AI-native architecture from the start in 2026, rather than retrofitting. That's a healthy trend. The retrofits I've had to do on applications that weren't designed with AI in mind are painful and expensive.",[30,581],{},[15,583,585],{"id":584},"_5-evaluation-and-observability-are-now-mandatory","5. Evaluation and Observability Are Now Mandatory",[20,587,588],{},"If I had to pick one trend that separates serious AI development from amateur AI development in 2026, it's this: serious teams have evaluation pipelines and observability into their AI systems. Amateur teams ship prompts and hope.",[20,590,591],{},"Evaluation means systematically testing AI outputs against known-good examples. It means tracking when models regress after updates. It means having metrics for output quality that go beyond \"does it seem right to me.\"",[20,593,594],{},"Observability means being able to trace a user's input through the system to the model call, see the full prompt that was constructed, the response that came back, and any post-processing that happened. When an AI feature behaves unexpectedly, you need to be able to debug it with the same rigor as any other system component.",[20,596,597],{},"This is an area where I've invested significant tooling effort. The frameworks that support this well — including Anthropic's evaluation tooling and the emerging ecosystem of LLM observability platforms — are maturing rapidly.",[30,599],{},[15,601,603],{"id":602},"_6-fine-tuning-is-getting-practical-for-domain-specific-applications","6. Fine-Tuning Is Getting Practical for Domain-Specific Applications",[20,605,606],{},"General-purpose models are excellent for general-purpose tasks. But for highly specialized domains — legal, medical, engineering, finance — the quality gap between a general model and a fine-tuned domain model is significant and getting more exploitable.",[20,608,609],{},"In 2026, fine-tuning workflows have become practical for organizations with even modest AI engineering capacity. The tooling is better, the costs are lower, and the infrastructure for serving fine-tuned models is mature. For software architects, this means domain-specific AI features are now achievable without a dedicated research team.",[20,611,612],{},"I'm working on projects that involve fine-tuned models for specific business domains, and the results compared to prompt-engineering-only approaches are meaningful. This is not hype — it's a real capability that has crossed the threshold of practical accessibility.",[30,614],{},[15,616,618],{"id":617},"_7-security-is-catching-up-to-the-threat-surface","7. Security Is Catching Up to the Threat Surface",[20,620,621],{},"The security implications of AI systems have lagged behind adoption, but in 2026, that's changing. Prompt injection, data exfiltration via AI interfaces, model output manipulation — these are real attack vectors that security teams are starting to account for.",[20,623,624],{},"For software architects building AI features, this means treating the AI layer with the same security discipline as any other system boundary. Input sanitization, output validation, access control on what context the model can see, audit logging of all model interactions — these are not optional in production systems.",[20,626,627],{},"I audit the AI integration layer in every serious project I work on. The attack surface is real and the consequences of getting it wrong range from embarrassing to catastrophic depending on what data the model has access to.",[30,629],{},[15,631,633],{"id":632},"what-to-actually-do-with-this","What to Actually Do With This",[20,635,636],{},"These trends are not independent — they compound. The teams winning in AI-augmented software development in 2026 are the ones treating AI as a serious engineering discipline with architecture, observability, security, and evaluation requirements, not as a feature to be added by dropping in an API call.",[20,638,639],{},"That's a shift in mindset that requires either hiring people who think this way or working with partners who do. It's the difference between an AI feature that creates competitive advantage and one that creates technical debt.",[20,641,642],{},"If you're thinking about how AI should fit into your software roadmap this year and you want a frank conversation about what's realistic vs. What's hype, I'm happy to have it.",[20,644,645,649],{},[197,646,648],{"href":199,"rel":647},[201],"Schedule a free consultation at Calendly"," and we'll talk through your specific situation — no sales pitch, just honest architecture thinking.",[30,651],{},[15,653,209],{"id":208},[211,655,656,660,666,672],{},[214,657,658],{},[197,659,468],{"href":467},[214,661,662],{},[197,663,665],{"href":664},"/blog/future-of-software-development-ai","The Future of Software Development in an AI World",[214,667,668],{},[197,669,671],{"href":670},"/blog/machine-learning-enterprise-software","Machine Learning in Enterprise Software: Where It Adds Real Value",[214,673,674],{},[197,675,677],{"href":676},"/blog/ai-ethics-enterprise-software","AI Ethics in Enterprise Software: The Practical Side of Responsible AI",{"title":239,"searchDepth":240,"depth":240,"links":679},[680,681,682,683,684,685,686,687,688,689],{"id":509,"depth":243,"text":510},{"id":524,"depth":243,"text":525},{"id":539,"depth":243,"text":540},{"id":554,"depth":243,"text":555},{"id":569,"depth":243,"text":570},{"id":584,"depth":243,"text":585},{"id":602,"depth":243,"text":603},{"id":617,"depth":243,"text":618},{"id":632,"depth":243,"text":633},{"id":208,"depth":243,"text":209},"A working software architect's take on the AI development trends that actually matter in 2026 — not hype, but patterns reshaping how software gets built.",[692,693],"ai software development trends","ai software development",{},{"title":231,"description":690},"blog/ai-software-development-trends-2026",[256,698,699,700,500],"Software Development","Trends","Enterprise Software","N_ITeanjJ3rqo7DThzzPslvqbEWFAYFUC71_nciiyO4",{"id":703,"title":704,"author":705,"body":706,"category":1328,"date":257,"description":1329,"extension":259,"featured":260,"image":261,"keywords":1330,"meta":1336,"navigation":266,"path":1337,"readTime":1338,"seo":1339,"stem":1340,"tags":1341,"__hash__":1346},"blog/blog/api-design-best-practices.md","API Design Best Practices That Survive Production",{"name":9,"bio":10},{"type":12,"value":707,"toc":1312},[708,712,715,718,721,723,727,730,733,743,746,752,755,759,762,768,779,781,785,788,802,811,820,823,830,832,836,839,842,930,933,954,958,1030,1032,1036,1039,1045,1118,1132,1135,1137,1141,1144,1150,1156,1162,1165,1187,1189,1193,1196,1199,1205,1211,1217,1223,1229,1232,1234,1238,1241,1247,1257,1259,1263,1266,1269,1271,1278,1280,1282,1308],[15,709,711],{"id":710},"apis-are-forever-or-close-enough","APIs Are Forever (or Close Enough)",[20,713,714],{},"When you ship a public API, you're making a contract. Clients — internal teams, partners, third-party developers — will build on top of that contract. Changing it later means breaking their code, coordinating migrations, and managing multiple versions simultaneously. This is expensive.",[20,716,717],{},"The decisions you make during API design have an outsized effect on how much pain you're managing two years from now. Most APIs I've audited have the same handful of problems: inconsistent error structures, no versioning strategy, opaque pagination, authentication that creates friction, and documentation that lags so far behind the implementation it's misleading. All of these are preventable.",[20,719,720],{},"Here's what actually matters.",[30,722],{},[15,724,726],{"id":725},"resource-design-think-nouns-not-verbs","Resource Design: Think Nouns, Not Verbs",[20,728,729],{},"REST APIs model resources, not procedures. The URL identifies what you're acting on; the HTTP method identifies what you're doing to it.",[20,731,732],{},"Good:",[734,735,740],"pre",{"className":736,"code":738,"language":739},[737],"language-text","GET /orders → list orders\nPOST /orders → create an order\nGET /orders/{id} → get a specific order\nPATCH /orders/{id} → update an order\nDELETE /orders/{id} → cancel/delete an order\n","text",[741,742,738],"code",{"__ignoreMap":239},[20,744,745],{},"Avoid:",[734,747,750],{"className":748,"code":749,"language":739},[737],"POST /getOrders\nPOST /createOrder\nPOST /updateOrderStatus\nPOST /cancelOrder\n",[741,751,749],{"__ignoreMap":239},[20,753,754],{},"The POST-everything style is common in older RPC-style APIs and JSON-RPC interfaces. It's not inherently wrong, but it abandons the semantic clarity that HTTP methods provide — and that clients use to understand what to expect.",[52,756,758],{"id":757},"nested-resources","Nested Resources",[20,760,761],{},"Use nesting for resources that only exist in the context of a parent:",[734,763,766],{"className":764,"code":765,"language":739},[737],"GET /orders/{orderId}/items\nPOST /orders/{orderId}/items\nGET /orders/{orderId}/items/{itemId}\n",[741,767,765],{"__ignoreMap":239},[20,769,770,771,774,775,778],{},"Don't nest more than two levels deep. ",[741,772,773],{},"GET /users/{id}/orders/{orderId}/items/{itemId}/reviews"," is technically correct and practically confusing. Beyond two levels, consider flattening: ",[741,776,777],{},"GET /items/{itemId}/reviews",".",[30,780],{},[15,782,784],{"id":783},"versioning-make-a-decision-and-commit-to-it","Versioning: Make a Decision and Commit to It",[20,786,787],{},"Every API that will have consumers needs a versioning strategy. The three common approaches:",[20,789,790,793,794,797,798,801],{},[123,791,792],{},"URL versioning:"," ",[741,795,796],{},"/api/v1/orders",", ",[741,799,800],{},"/api/v2/orders",". Explicit, easy to route, easy to understand. The downside: clients have to update their URLs when you version. This is the approach I use most often because it's the most explicit.",[20,803,804,793,807,810],{},[123,805,806],{},"Header versioning:",[741,808,809],{},"Accept: application/vnd.myapp.v2+json",". Clean URLs, but requires clients to set custom headers and makes version debugging harder.",[20,812,813,793,816,819],{},[123,814,815],{},"Query parameter versioning:",[741,817,818],{},"/api/orders?version=2",". Simple but the least RESTful and easiest to forget.",[20,821,822],{},"The right choice depends on your consumers. Internal APIs where you control all clients can use any approach. Public APIs with external consumers benefit from URL versioning because it's visible in logs and easy to document.",[20,824,825,826,829],{},"Commit to the strategy before you ship v1. Once consumers are building on ",[741,827,828],{},"/api/orders",", adding versioning is painful.",[30,831],{},[15,833,835],{"id":834},"error-handling-be-specific-and-consistent","Error Handling: Be Specific and Consistent",[20,837,838],{},"Inconsistent error handling is the fastest way to make your API a frustration. I've worked with APIs where a 404 meant \"resource not found,\" a 400 returned a plain string, and a 500 returned an HTML error page. Clients had to write special-case handling for every error type.",[20,840,841],{},"Pick a consistent error structure and use it everywhere:",[734,843,847],{"className":844,"code":845,"language":846,"meta":239,"style":239},"language-json shiki shiki-themes github-dark","{\n \"error\": {\n \"code\": \"ORDER_NOT_FOUND\",\n \"message\": \"No order found with the provided ID.\",\n \"field\": null,\n \"requestId\": \"req_1a2b3c4d5e\"\n }\n}\n","json",[741,848,849,858,867,882,895,908,919,925],{"__ignoreMap":239},[850,851,854],"span",{"class":852,"line":853},"line",1,[850,855,857],{"class":856},"s95oV","{\n",[850,859,860,864],{"class":852,"line":243},[850,861,863],{"class":862},"sDLfK"," \"error\"",[850,865,866],{"class":856},": {\n",[850,868,869,872,875,879],{"class":852,"line":240},[850,870,871],{"class":862}," \"code\"",[850,873,874],{"class":856},": ",[850,876,878],{"class":877},"sU2Wk","\"ORDER_NOT_FOUND\"",[850,880,881],{"class":856},",\n",[850,883,885,888,890,893],{"class":852,"line":884},4,[850,886,887],{"class":862}," \"message\"",[850,889,874],{"class":856},[850,891,892],{"class":877},"\"No order found with the provided ID.\"",[850,894,881],{"class":856},[850,896,898,901,903,906],{"class":852,"line":897},5,[850,899,900],{"class":862}," \"field\"",[850,902,874],{"class":856},[850,904,905],{"class":862},"null",[850,907,881],{"class":856},[850,909,911,914,916],{"class":852,"line":910},6,[850,912,913],{"class":862}," \"requestId\"",[850,915,874],{"class":856},[850,917,918],{"class":877},"\"req_1a2b3c4d5e\"\n",[850,920,922],{"class":852,"line":921},7,[850,923,924],{"class":856}," }\n",[850,926,927],{"class":852,"line":268},[850,928,929],{"class":856},"}\n",[20,931,932],{},"Key principles:",[211,934,935,938,941,944,951],{},[214,936,937],{},"Use appropriate HTTP status codes (don't return 200 with an error in the body)",[214,939,940],{},"Include a machine-readable error code for programmatic handling",[214,942,943],{},"Include a human-readable message for debugging",[214,945,946,947,950],{},"Include a ",[741,948,949],{},"requestId"," so clients can report specific failures to your support team",[214,952,953],{},"For validation errors, include field-level errors so clients can display inline error messages",[52,955,957],{"id":956},"http-status-code-usage","HTTP Status Code Usage",[211,959,960,966,976,982,988,994,1000,1006,1012,1018,1024],{},[214,961,962,965],{},[741,963,964],{},"200 OK"," — successful GET, PATCH, PUT",[214,967,968,971,972,975],{},[741,969,970],{},"201 Created"," — successful POST that creates a resource; include ",[741,973,974],{},"Location"," header pointing to the new resource",[214,977,978,981],{},[741,979,980],{},"204 No Content"," — successful DELETE or action with no response body",[214,983,984,987],{},[741,985,986],{},"400 Bad Request"," — malformed request, validation errors",[214,989,990,993],{},[741,991,992],{},"401 Unauthorized"," — authentication required or token invalid",[214,995,996,999],{},[741,997,998],{},"403 Forbidden"," — authenticated but not authorized for this action",[214,1001,1002,1005],{},[741,1003,1004],{},"404 Not Found"," — resource doesn't exist",[214,1007,1008,1011],{},[741,1009,1010],{},"409 Conflict"," — state conflict (duplicate, version mismatch)",[214,1013,1014,1017],{},[741,1015,1016],{},"422 Unprocessable Entity"," — request is syntactically valid but semantically wrong",[214,1019,1020,1023],{},[741,1021,1022],{},"429 Too Many Requests"," — rate limit exceeded",[214,1025,1026,1029],{},[741,1027,1028],{},"500 Internal Server Error"," — your fault, not the client's",[30,1031],{},[15,1033,1035],{"id":1034},"pagination-dont-return-unbounded-collections","Pagination: Don't Return Unbounded Collections",[20,1037,1038],{},"Never return all records in a collection endpoint. A collection that works fine at 100 records will destroy your API and your database at 100,000.",[20,1040,1041,1044],{},[123,1042,1043],{},"Cursor-based pagination"," is the most scalable approach. Return a cursor pointing to the last item, and the client passes it back to get the next page:",[734,1046,1048],{"className":844,"code":1047,"language":846,"meta":239,"style":239},"{\n \"data\": [...],\n \"pagination\": {\n \"cursor\": \"eyJpZCI6MTAwfQ==\",\n \"hasMore\": true,\n \"limit\": 20\n }\n}\n",[741,1049,1050,1054,1069,1076,1088,1100,1110,1114],{"__ignoreMap":239},[850,1051,1052],{"class":852,"line":853},[850,1053,857],{"class":856},[850,1055,1056,1059,1062,1066],{"class":852,"line":243},[850,1057,1058],{"class":862}," \"data\"",[850,1060,1061],{"class":856},": [",[850,1063,1065],{"class":1064},"s6RL2","...",[850,1067,1068],{"class":856},"],\n",[850,1070,1071,1074],{"class":852,"line":240},[850,1072,1073],{"class":862}," \"pagination\"",[850,1075,866],{"class":856},[850,1077,1078,1081,1083,1086],{"class":852,"line":884},[850,1079,1080],{"class":862}," \"cursor\"",[850,1082,874],{"class":856},[850,1084,1085],{"class":877},"\"eyJpZCI6MTAwfQ==\"",[850,1087,881],{"class":856},[850,1089,1090,1093,1095,1098],{"class":852,"line":897},[850,1091,1092],{"class":862}," \"hasMore\"",[850,1094,874],{"class":856},[850,1096,1097],{"class":862},"true",[850,1099,881],{"class":856},[850,1101,1102,1105,1107],{"class":852,"line":910},[850,1103,1104],{"class":862}," \"limit\"",[850,1106,874],{"class":856},[850,1108,1109],{"class":862},"20\n",[850,1111,1112],{"class":852,"line":921},[850,1113,924],{"class":856},[850,1115,1116],{"class":852,"line":268},[850,1117,929],{"class":856},[20,1119,1120,1123,1124,1127,1128,1131],{},[123,1121,1122],{},"Offset pagination"," (",[741,1125,1126],{},"?page=3&limit=20",") is simpler and familiar but has problems at scale: page drift when records are inserted or deleted, and expensive ",[741,1129,1130],{},"OFFSET"," queries that become slow at large offsets.",[20,1133,1134],{},"For most APIs that will have moderate-to-large datasets, start with cursor pagination. It's more work upfront but avoids painful migrations later.",[30,1136],{},[15,1138,1140],{"id":1139},"authentication-dont-reinvent-it","Authentication: Don't Reinvent It",[20,1142,1143],{},"API authentication is a solved problem. Don't invent a custom scheme unless you have a specific reason.",[20,1145,1146,1149],{},[123,1147,1148],{},"For internal or partner APIs:"," JWTs with short expiry (15 minutes) and refresh tokens. Include the minimum necessary claims in the payload — don't put everything in the JWT; it bloats every request header and becomes a source of stale data when claims change.",[20,1151,1152,1155],{},[123,1153,1154],{},"For public APIs:"," OAuth 2.0 with the appropriate grant type. Authorization Code with PKCE for user-facing applications, Client Credentials for machine-to-machine.",[20,1157,1158,1161],{},[123,1159,1160],{},"For simple internal services:"," Static API keys stored server-side, rotatable on demand.",[20,1163,1164],{},"Regardless of the approach:",[211,1166,1167,1170,1173,1176],{},[214,1168,1169],{},"Always use HTTPS — never accept auth tokens over plain HTTP",[214,1171,1172],{},"Set appropriate token expiry and force rotation",[214,1174,1175],{},"Include scopes so clients only get the permissions they need",[214,1177,1178,1179,1182,1183,1186],{},"Return ",[741,1180,1181],{},"401"," (authentication) vs ",[741,1184,1185],{},"403"," (authorization) correctly — they mean different things",[30,1188],{},[15,1190,1192],{"id":1191},"documentation-write-it-like-your-support-inbox-depends-on-it","Documentation: Write It Like Your Support Inbox Depends on It",[20,1194,1195],{},"It does. The quality of your API documentation directly determines how many support questions your team fields, how many integration errors your partners make, and how long it takes developers to get productive.",[20,1197,1198],{},"Good API documentation includes:",[20,1200,1201,1204],{},[123,1202,1203],{},"Authentication instructions"," that are step-by-step, with examples.",[20,1206,1207,1210],{},[123,1208,1209],{},"Endpoint reference"," with every parameter documented, including type, whether it's required, and valid values. Include example request and response bodies for every endpoint — not generic templates, actual representative examples.",[20,1212,1213,1216],{},[123,1214,1215],{},"Error code reference"," listing every error code your API can return and what a client should do about each one.",[20,1218,1219,1222],{},[123,1220,1221],{},"Getting started guide"," that takes a new developer from zero to their first successful API call in 15 minutes or less.",[20,1224,1225,1228],{},[123,1226,1227],{},"Change log"," noting what changed in each API version.",[20,1230,1231],{},"Tools like OpenAPI/Swagger make the reference portion maintainable by generating it from code annotations. Use them. But don't confuse generated reference docs with actual documentation — the conceptual guides, authentication walkthrough, and error reference require human writing.",[30,1233],{},[15,1235,1237],{"id":1236},"rate-limiting-protect-yourself-and-be-transparent","Rate Limiting: Protect Yourself and Be Transparent",[20,1239,1240],{},"Every API exposed outside your infrastructure needs rate limiting. Communicate limits in response headers:",[734,1242,1245],{"className":1243,"code":1244,"language":739},[737],"X-RateLimit-Limit: 1000\nX-RateLimit-Remaining: 847\nX-RateLimit-Reset: 1709510400\n",[741,1246,1244],{"__ignoreMap":239},[20,1248,1249,1250,1252,1253,1256],{},"When a client exceeds the limit, return ",[741,1251,1022],{}," with a ",[741,1254,1255],{},"Retry-After"," header indicating when they can try again. This gives clients everything they need to implement backoff without guessing.",[30,1258],{},[15,1260,1262],{"id":1261},"the-api-that-earns-trust","The API That Earns Trust",[20,1264,1265],{},"The common thread through all of these practices is predictability. A good API does what clients expect, fails in ways they can handle, and evolves in ways they can adapt to without breaking their code. That predictability is what makes an API trustworthy, and trust is what makes external developers build on your platform.",[20,1267,1268],{},"Design for the developer who will consume your API at 2am when a production issue is happening. If they can figure out what went wrong from your error response, you've done your job.",[30,1270],{},[20,1272,1273,1274],{},"If you're designing a new API or auditing an existing one, I work with teams on API strategy and design. ",[197,1275,1277],{"href":199,"rel":1276},[201],"Let's schedule time to talk.",[30,1279],{},[15,1281,209],{"id":208},[211,1283,1284,1290,1296,1302],{},[214,1285,1286],{},[197,1287,1289],{"href":1288},"/blog/api-gateway-patterns","API Gateway Patterns: More Than Just a Reverse Proxy",[214,1291,1292],{},[197,1293,1295],{"href":1294},"/blog/domain-driven-design-guide","Domain-Driven Design in Practice (Without the Theory Overload)",[214,1297,1298],{},[197,1299,1301],{"href":1300},"/blog/design-patterns-for-architects","Software Design Patterns Every Architect Should Have in Their Toolkit",[214,1303,1304],{},[197,1305,1307],{"href":1306},"/blog/system-design-interview-guide","System Design Interviews: What They're Actually Testing",[1309,1310,1311],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s6RL2, html code.shiki .s6RL2{--shiki-default:#FDAEB7;--shiki-default-font-style:italic}",{"title":239,"searchDepth":240,"depth":240,"links":1313},[1314,1315,1318,1319,1322,1323,1324,1325,1326,1327],{"id":710,"depth":243,"text":711},{"id":725,"depth":243,"text":726,"children":1316},[1317],{"id":757,"depth":240,"text":758},{"id":783,"depth":243,"text":784},{"id":834,"depth":243,"text":835,"children":1320},[1321],{"id":956,"depth":240,"text":957},{"id":1034,"depth":243,"text":1035},{"id":1139,"depth":243,"text":1140},{"id":1191,"depth":243,"text":1192},{"id":1236,"depth":243,"text":1237},{"id":1261,"depth":243,"text":1262},{"id":208,"depth":243,"text":209},"Architecture","API design best practices aren't just about clean URLs — they're about creating interfaces that are predictable, resilient, and easy to evolve. Here's what actually matters in production.",[1331,1332,1333,1334,1335],"api design best practices","REST API design","API versioning","API error handling","API documentation",{},"/blog/api-design-best-practices",9,{"title":704,"description":1329},"blog/api-design-best-practices",[1342,1343,1344,1345],"API Design","REST","Backend Development","Software Architecture","YLmrMHxiKxkx507wT8MeLpHKP8lsRqovMugs7PJUTSo",{"id":1348,"title":1349,"author":1350,"body":1351,"category":2154,"date":257,"description":2155,"extension":259,"featured":260,"image":261,"keywords":2156,"meta":2159,"navigation":266,"path":2160,"readTime":1495,"seo":2161,"stem":2162,"tags":2163,"__hash__":2167},"blog/blog/api-first-architecture.md","API-First Architecture: Building Software That Integrates by Default",{"name":9,"bio":10},{"type":12,"value":1352,"toc":2143},[1353,1357,1360,1363,1366,1369,1373,1376,1382,1388,1394,1400,1404,1407,1410,1413,1416,1619,1622,1626,1629,1635,1641,1644,1790,1793,1797,1800,1817,1823,1829,1931,1937,1946,1950,1953,1956,1959,1965,2043,2049,2055,2061,2065,2068,2070,2087,2090,2094,2097,2100,2103,2110,2112,2114,2140],[15,1354,1356],{"id":1355},"the-integration-tax-youre-paying","The Integration Tax You're Paying",[20,1358,1359],{},"Every enterprise software system eventually needs to integrate with other systems. The question isn't whether integration will be required — it's whether your system was designed for it.",[20,1361,1362],{},"Systems that weren't designed for integration impose an integration tax on every project that needs to connect to them. The data model wasn't designed with external consumers in mind, so exports are awkward. Authentication isn't designed for machine-to-machine access, so integrations use workarounds. Webhooks weren't built in, so integrations poll for changes. Error responses aren't consistent, so every integration builds custom error handling.",[20,1364,1365],{},"API-first architecture flips this by treating integration as a first-class design concern from the beginning. The API is not an afterthought built when someone needs it — it's the primary interface, and the UI is just one of its consumers.",[20,1367,1368],{},"Here's what API-first looks like in practice and why it produces better systems.",[15,1370,1372],{"id":1371},"what-api-first-actually-means","What API-First Actually Means",[20,1374,1375],{},"API-first is a design principle, not a technology choice. It means:",[20,1377,1378,1381],{},[123,1379,1380],{},"The API is defined before the implementation."," You design the API interface — the endpoints, request/response shapes, error codes, authentication model — before you write any implementation code. This is often done with an API specification language like OpenAPI. The specification becomes a contract between the API team and any consumer (the UI team, integration partners, mobile developers).",[20,1383,1384,1387],{},[123,1385,1386],{},"The UI consumes the same API that external consumers use."," There is no separate \"internal API\" for the frontend and a different \"external API\" for partners. One API, one contract, one truth. This forces the API to be good — if it's painful to use from your own frontend, it's painful for everyone.",[20,1389,1390,1393],{},[123,1391,1392],{},"API design is a first-class engineering concern."," API design decisions get the same level of review and scrutiny as architecture decisions. A bad API is architectural debt that propagates to every consumer.",[20,1395,1396,1399],{},[123,1397,1398],{},"Breaking changes are treated seriously."," The API is a contract. Changing it breaks consumers. API versioning and deprecation policies exist and are followed.",[15,1401,1403],{"id":1402},"designing-the-api-before-the-database","Designing the API Before the Database",[20,1405,1406],{},"The most common mistake in enterprise software development is letting the database schema drive the API design. The database schema gets built to model the domain, and then the API is a thin layer over the database — endpoints map directly to tables, request/response shapes mirror the schema.",[20,1408,1409],{},"This produces APIs that are hard to use. The database schema is optimized for storage, not for consumption. It reflects internal concerns (foreign keys, normalization, audit columns) that consumers don't need and shouldn't see. It changes as the domain evolves, and every change breaks consumers.",[20,1411,1412],{},"API-first design inverts this. You start by asking: what does a consumer need to accomplish, and what's the cleanest interface for accomplishing it?",[20,1414,1415],{},"A consumer creating an order doesn't want to know about your database's order-line normalization. They want to POST a single request with the order details and receive a response with the order ID and status. The API design reflects the consumer's model, and the database schema is designed to support the API, not the other way around.",[734,1417,1421],{"className":1418,"code":1419,"language":1420,"meta":239,"style":239},"language-yaml shiki shiki-themes github-dark","# API specification (OpenAPI) defines the contract\npaths:\n /orders:\n post:\n summary: Create a new order\n requestBody:\n required: true\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/CreateOrderRequest'\n responses:\n '201':\n description: Order created successfully\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/OrderResponse'\n '422':\n description: Validation error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/ValidationError'\n","yaml",[741,1422,1423,1429,1438,1445,1452,1462,1469,1479,1486,1493,1501,1512,1520,1528,1539,1546,1553,1560,1570,1578,1588,1595,1602,1609],{"__ignoreMap":239},[850,1424,1425],{"class":852,"line":853},[850,1426,1428],{"class":1427},"sAwPA","# API specification (OpenAPI) defines the contract\n",[850,1430,1431,1435],{"class":852,"line":243},[850,1432,1434],{"class":1433},"s4JwU","paths",[850,1436,1437],{"class":856},":\n",[850,1439,1440,1443],{"class":852,"line":240},[850,1441,1442],{"class":1433}," /orders",[850,1444,1437],{"class":856},[850,1446,1447,1450],{"class":852,"line":884},[850,1448,1449],{"class":1433}," post",[850,1451,1437],{"class":856},[850,1453,1454,1457,1459],{"class":852,"line":897},[850,1455,1456],{"class":1433}," summary",[850,1458,874],{"class":856},[850,1460,1461],{"class":877},"Create a new order\n",[850,1463,1464,1467],{"class":852,"line":910},[850,1465,1466],{"class":1433}," requestBody",[850,1468,1437],{"class":856},[850,1470,1471,1474,1476],{"class":852,"line":921},[850,1472,1473],{"class":1433}," required",[850,1475,874],{"class":856},[850,1477,1478],{"class":862},"true\n",[850,1480,1481,1484],{"class":852,"line":268},[850,1482,1483],{"class":1433}," content",[850,1485,1437],{"class":856},[850,1487,1488,1491],{"class":852,"line":1338},[850,1489,1490],{"class":1433}," application/json",[850,1492,1437],{"class":856},[850,1494,1496,1499],{"class":852,"line":1495},10,[850,1497,1498],{"class":1433}," schema",[850,1500,1437],{"class":856},[850,1502,1504,1507,1509],{"class":852,"line":1503},11,[850,1505,1506],{"class":1433}," $ref",[850,1508,874],{"class":856},[850,1510,1511],{"class":877},"'#/components/schemas/CreateOrderRequest'\n",[850,1513,1515,1518],{"class":852,"line":1514},12,[850,1516,1517],{"class":1433}," responses",[850,1519,1437],{"class":856},[850,1521,1523,1526],{"class":852,"line":1522},13,[850,1524,1525],{"class":877}," '201'",[850,1527,1437],{"class":856},[850,1529,1531,1534,1536],{"class":852,"line":1530},14,[850,1532,1533],{"class":1433}," description",[850,1535,874],{"class":856},[850,1537,1538],{"class":877},"Order created successfully\n",[850,1540,1542,1544],{"class":852,"line":1541},15,[850,1543,1483],{"class":1433},[850,1545,1437],{"class":856},[850,1547,1549,1551],{"class":852,"line":1548},16,[850,1550,1490],{"class":1433},[850,1552,1437],{"class":856},[850,1554,1556,1558],{"class":852,"line":1555},17,[850,1557,1498],{"class":1433},[850,1559,1437],{"class":856},[850,1561,1563,1565,1567],{"class":852,"line":1562},18,[850,1564,1506],{"class":1433},[850,1566,874],{"class":856},[850,1568,1569],{"class":877},"'#/components/schemas/OrderResponse'\n",[850,1571,1573,1576],{"class":852,"line":1572},19,[850,1574,1575],{"class":877}," '422'",[850,1577,1437],{"class":856},[850,1579,1581,1583,1585],{"class":852,"line":1580},20,[850,1582,1533],{"class":1433},[850,1584,874],{"class":856},[850,1586,1587],{"class":877},"Validation error\n",[850,1589,1591,1593],{"class":852,"line":1590},21,[850,1592,1483],{"class":1433},[850,1594,1437],{"class":856},[850,1596,1598,1600],{"class":852,"line":1597},22,[850,1599,1490],{"class":1433},[850,1601,1437],{"class":856},[850,1603,1605,1607],{"class":852,"line":1604},23,[850,1606,1498],{"class":1433},[850,1608,1437],{"class":856},[850,1610,1612,1614,1616],{"class":852,"line":1611},24,[850,1613,1506],{"class":1433},[850,1615,874],{"class":856},[850,1617,1618],{"class":877},"'#/components/schemas/ValidationError'\n",[20,1620,1621],{},"The specification is the contract. The database schema implements whatever is needed to fulfill this contract.",[15,1623,1625],{"id":1624},"authentication-and-authorization-for-machine-consumers","Authentication and Authorization for Machine Consumers",[20,1627,1628],{},"Enterprise APIs need to authenticate both human users (accessing via a frontend) and machine consumers (integrations, automation, other services). These require different authentication mechanisms.",[20,1630,1631,1634],{},[123,1632,1633],{},"Human users:"," OAuth 2.0 with authorization code flow, JWTs for session management, refresh token rotation. The user authenticates once, gets a token, and subsequent requests are authenticated by the token.",[20,1636,1637,1640],{},[123,1638,1639],{},"Machine consumers:"," OAuth 2.0 client credentials flow, or API keys. Machine consumers don't have a user to authenticate — they authenticate with a client ID and secret that represents the integration itself.",[20,1642,1643],{},"The authorization model — what each authenticated party is allowed to do — should be the same regardless of how they authenticated. A machine consumer performing an integration should be subject to the same business rules and data access controls as a human user.",[734,1645,1649],{"className":1646,"code":1647,"language":1648,"meta":239,"style":239},"language-typescript shiki shiki-themes github-dark","// The authorization check is independent of authentication mechanism\nasync function authorizeOrderCreation(\n actorId: string,\n actorType: 'user' | 'service',\n tenantId: string\n): Promise\u003Cboolean> {\n const permissions = await getPermissions(actorId, actorType);\n return permissions.includes('orders:write')\n && await actorBelongsToTenant(actorId, tenantId);\n}\n","typescript",[741,1650,1651,1656,1672,1686,1704,1714,1733,1753,1773,1786],{"__ignoreMap":239},[850,1652,1653],{"class":852,"line":853},[850,1654,1655],{"class":1427},"// The authorization check is independent of authentication mechanism\n",[850,1657,1658,1662,1665,1669],{"class":852,"line":243},[850,1659,1661],{"class":1660},"snl16","async",[850,1663,1664],{"class":1660}," function",[850,1666,1668],{"class":1667},"svObZ"," authorizeOrderCreation",[850,1670,1671],{"class":856},"(\n",[850,1673,1674,1678,1681,1684],{"class":852,"line":240},[850,1675,1677],{"class":1676},"s9osk"," actorId",[850,1679,1680],{"class":1660},":",[850,1682,1683],{"class":862}," string",[850,1685,881],{"class":856},[850,1687,1688,1691,1693,1696,1699,1702],{"class":852,"line":884},[850,1689,1690],{"class":1676}," actorType",[850,1692,1680],{"class":1660},[850,1694,1695],{"class":877}," 'user'",[850,1697,1698],{"class":1660}," |",[850,1700,1701],{"class":877}," 'service'",[850,1703,881],{"class":856},[850,1705,1706,1709,1711],{"class":852,"line":897},[850,1707,1708],{"class":1676}," tenantId",[850,1710,1680],{"class":1660},[850,1712,1713],{"class":862}," string\n",[850,1715,1716,1719,1721,1724,1727,1730],{"class":852,"line":910},[850,1717,1718],{"class":856},")",[850,1720,1680],{"class":1660},[850,1722,1723],{"class":1667}," Promise",[850,1725,1726],{"class":856},"\u003C",[850,1728,1729],{"class":862},"boolean",[850,1731,1732],{"class":856},"> {\n",[850,1734,1735,1738,1741,1744,1747,1750],{"class":852,"line":921},[850,1736,1737],{"class":1660}," const",[850,1739,1740],{"class":862}," permissions",[850,1742,1743],{"class":1660}," =",[850,1745,1746],{"class":1660}," await",[850,1748,1749],{"class":1667}," getPermissions",[850,1751,1752],{"class":856},"(actorId, actorType);\n",[850,1754,1755,1758,1761,1764,1767,1770],{"class":852,"line":268},[850,1756,1757],{"class":1660}," return",[850,1759,1760],{"class":856}," permissions.",[850,1762,1763],{"class":1667},"includes",[850,1765,1766],{"class":856},"(",[850,1768,1769],{"class":877},"'orders:write'",[850,1771,1772],{"class":856},")\n",[850,1774,1775,1778,1780,1783],{"class":852,"line":1338},[850,1776,1777],{"class":1660}," &&",[850,1779,1746],{"class":1660},[850,1781,1782],{"class":1667}," actorBelongsToTenant",[850,1784,1785],{"class":856},"(actorId, tenantId);\n",[850,1787,1788],{"class":852,"line":1495},[850,1789,929],{"class":856},[20,1791,1792],{},"One practical recommendation: issue API keys with scopes. An integration that only needs to read order status shouldn't have access to create or modify orders. Scoped API keys limit blast radius if a key is compromised and make the integration's authorization explicit and auditable.",[15,1794,1796],{"id":1795},"request-and-response-design","Request and Response Design",[20,1798,1799],{},"Good API design is partly taste and partly discipline. Here are the principles I apply consistently:",[20,1801,1802,1805,1806,1809,1810,1809,1813,1816],{},[123,1803,1804],{},"Be consistent across all endpoints."," If some endpoints use ",[741,1807,1808],{},"createdAt"," and others use ",[741,1811,1812],{},"created_at",[741,1814,1815],{},"dateCreated",", the API is inconsistent and error-prone. Choose a convention and apply it everywhere. I use camelCase for REST APIs because JSON consumers are typically JavaScript.",[20,1818,1819,1822],{},[123,1820,1821],{},"Return enough information to be actionable, not everything in the database."," A response should include the fields a reasonable consumer needs for their workflow. Not every database column. Not nothing. This requires designing for the consumer's use cases, which is why API design should happen before implementation.",[20,1824,1825,1828],{},[123,1826,1827],{},"Error responses must be consistent and informative."," Every error response should have: an HTTP status code that reflects the error category, a machine-readable error code that identifies the specific error, and a human-readable message that explains what happened. Validation errors should identify which fields failed and why.",[734,1830,1832],{"className":1646,"code":1831,"language":1648,"meta":239,"style":239},"// Consistent error response structure\ninterface ApiError {\n code: string; // Machine-readable: 'VALIDATION_ERROR', 'NOT_FOUND', etc. Message: string; // Human-readable description\n details?: Array\u003C{ // Field-level details for validation errors\n field: string;\n message: string;\n }>;\n requestId: string; // For support/debugging correlation\n}\n",[741,1833,1834,1839,1850,1868,1885,1897,1908,1913,1927],{"__ignoreMap":239},[850,1835,1836],{"class":852,"line":853},[850,1837,1838],{"class":1427},"// Consistent error response structure\n",[850,1840,1841,1844,1847],{"class":852,"line":243},[850,1842,1843],{"class":1660},"interface",[850,1845,1846],{"class":1667}," ApiError",[850,1848,1849],{"class":856}," {\n",[850,1851,1852,1855,1857,1859,1862,1865],{"class":852,"line":240},[850,1853,1854],{"class":1676}," code",[850,1856,1680],{"class":1660},[850,1858,1683],{"class":862},[850,1860,1861],{"class":856},"; ",[850,1863,1864],{"class":1427},"// Machine-readable: 'VALIDATION_ERROR', 'NOT_FOUND', etc. Message: string;",[850,1866,1867],{"class":1427}," // Human-readable description\n",[850,1869,1870,1873,1876,1879,1882],{"class":852,"line":884},[850,1871,1872],{"class":1676}," details",[850,1874,1875],{"class":1660},"?:",[850,1877,1878],{"class":1667}," Array",[850,1880,1881],{"class":856},"\u003C{ ",[850,1883,1884],{"class":1427},"// Field-level details for validation errors\n",[850,1886,1887,1890,1892,1894],{"class":852,"line":897},[850,1888,1889],{"class":1676}," field",[850,1891,1680],{"class":1660},[850,1893,1683],{"class":862},[850,1895,1896],{"class":856},";\n",[850,1898,1899,1902,1904,1906],{"class":852,"line":910},[850,1900,1901],{"class":1676}," message",[850,1903,1680],{"class":1660},[850,1905,1683],{"class":862},[850,1907,1896],{"class":856},[850,1909,1910],{"class":852,"line":921},[850,1911,1912],{"class":856}," }>;\n",[850,1914,1915,1918,1920,1922,1924],{"class":852,"line":268},[850,1916,1917],{"class":1676}," requestId",[850,1919,1680],{"class":1660},[850,1921,1683],{"class":862},[850,1923,1861],{"class":856},[850,1925,1926],{"class":1427},"// For support/debugging correlation\n",[850,1928,1929],{"class":852,"line":1338},[850,1930,929],{"class":856},[20,1932,1933,1936],{},[123,1934,1935],{},"Pagination is required for list endpoints."," Any endpoint that returns a list of resources must be paginated. Returning an unbounded list will eventually cause timeouts, memory issues, and poor consumer experience. Cursor-based pagination is preferable to offset-based for large datasets because it's stable (adding items doesn't shift pages) and performs better on large tables.",[20,1938,1939,1942,1943,1945],{},[123,1940,1941],{},"Versioning from day one."," Include the API version in the URL (",[741,1944,796],{},") or in the Accept header. Even if you never introduce a v2, the convention signals to consumers that you think about backwards compatibility. When you do need a v2, the infrastructure is already in place.",[15,1947,1949],{"id":1948},"webhooks-making-your-api-push-instead-of-pull","Webhooks: Making Your API Push Instead of Pull",[20,1951,1952],{},"A truly integration-friendly API doesn't make consumers poll for changes. It notifies them when things happen.",[20,1954,1955],{},"Webhooks are HTTP callbacks — when an event occurs in your system, you POST a notification to a URL the consumer registered. The consumer processes the notification immediately instead of discovering the change on their next poll cycle.",[20,1957,1958],{},"The webhook design decisions that matter:",[20,1960,1961,1964],{},[123,1962,1963],{},"Event schema consistency."," Every webhook event should have a consistent envelope: event type, event ID, timestamp, and the resource that changed. The consumer should be able to identify what happened from the envelope without parsing the payload.",[734,1966,1968],{"className":1646,"code":1967,"language":1648,"meta":239,"style":239},"interface WebhookEvent {\n id: string; // Unique event ID\n type: string; // 'order.created', 'order.status_changed', etc. Timestamp: string; // ISO 8601\n version: string; // Schema version for the payload\n data: unknown; // The resource that changed\n}\n",[741,1969,1970,1979,1993,2010,2024,2039],{"__ignoreMap":239},[850,1971,1972,1974,1977],{"class":852,"line":853},[850,1973,1843],{"class":1660},[850,1975,1976],{"class":1667}," WebhookEvent",[850,1978,1849],{"class":856},[850,1980,1981,1984,1986,1988,1990],{"class":852,"line":243},[850,1982,1983],{"class":1676}," id",[850,1985,1680],{"class":1660},[850,1987,1683],{"class":862},[850,1989,1861],{"class":856},[850,1991,1992],{"class":1427},"// Unique event ID\n",[850,1994,1995,1998,2000,2002,2004,2007],{"class":852,"line":240},[850,1996,1997],{"class":1676}," type",[850,1999,1680],{"class":1660},[850,2001,1683],{"class":862},[850,2003,1861],{"class":856},[850,2005,2006],{"class":1427},"// 'order.created', 'order.status_changed', etc. Timestamp: string;",[850,2008,2009],{"class":1427}," // ISO 8601\n",[850,2011,2012,2015,2017,2019,2021],{"class":852,"line":884},[850,2013,2014],{"class":1676}," version",[850,2016,1680],{"class":1660},[850,2018,1683],{"class":862},[850,2020,1861],{"class":856},[850,2022,2023],{"class":1427},"// Schema version for the payload\n",[850,2025,2026,2029,2031,2034,2036],{"class":852,"line":897},[850,2027,2028],{"class":1676}," data",[850,2030,1680],{"class":1660},[850,2032,2033],{"class":862}," unknown",[850,2035,1861],{"class":856},[850,2037,2038],{"class":1427},"// The resource that changed\n",[850,2040,2041],{"class":852,"line":910},[850,2042,929],{"class":856},[20,2044,2045,2048],{},[123,2046,2047],{},"Delivery guarantees."," Webhook delivery is at-least-once. Consumers must handle duplicate deliveries idempotently. Include an event ID they can use for idempotency.",[20,2050,2051,2054],{},[123,2052,2053],{},"Retry with exponential backoff."," When webhook delivery fails (consumer is down, returns an error), retry with increasing delays. Track delivery attempts and alert when a consumer has been failing for an extended period.",[20,2056,2057,2060],{},[123,2058,2059],{},"Signature verification."," Sign every webhook payload with an HMAC-SHA256 signature using a secret the consumer registered. This allows consumers to verify that the webhook came from your system and wasn't tampered with in transit.",[15,2062,2064],{"id":2063},"documentation-as-a-deliverable","Documentation as a Deliverable",[20,2066,2067],{},"An API without documentation is half a product. The consumers of your API — whether internal teams or external partners — need documentation that goes beyond the OpenAPI specification.",[20,2069,1198],{},[211,2071,2072,2075,2078,2081,2084],{},[214,2073,2074],{},"Getting started guide with authentication setup and a first API call",[214,2076,2077],{},"Use case guides (not just reference documentation) that walk through common integration scenarios",[214,2079,2080],{},"Code samples in the languages your consumers use",[214,2082,2083],{},"Error code reference with explanations and remediation",[214,2085,2086],{},"Changelog and deprecation notices",[20,2088,2089],{},"Generate reference documentation automatically from your OpenAPI specification using tools like Scalar, Swagger UI, or Redocly. Write the use case guides by hand — this is where you explain the why and the how, not just the what.",[15,2091,2093],{"id":2092},"the-competitive-advantage-of-api-first","The Competitive Advantage of API-First",[20,2095,2096],{},"Here's the business case that often doesn't get made: API-first systems integrate with the future. Systems that integrate well become hubs. New tools plug in. New workflows get automated. New products extend the platform. The integration cost for each new connection is lower because the foundation is designed for it.",[20,2098,2099],{},"Systems that weren't designed for integration become isolated. Each new integration is an expensive project. The organization gradually builds workarounds and parallel systems to compensate. The value of the system degrades relative to its potential.",[20,2101,2102],{},"API-first design is a strategic investment in the extensibility and longevity of your software.",[20,2104,2105,2106,778],{},"If you're designing an enterprise system and want to think through the API architecture — authentication model, versioning strategy, event system — ",[197,2107,2109],{"href":199,"rel":2108},[201],"schedule a conversation at calendly.com/jamesrossjr",[30,2111],{},[15,2113,209],{"id":208},[211,2115,2116,2122,2128,2134],{},[214,2117,2118],{},[197,2119,2121],{"href":2120},"/blog/multi-tenant-architecture","Multi-Tenant Architecture: Patterns for Building Software That Serves Many Clients",[214,2123,2124],{},[197,2125,2127],{"href":2126},"/blog/build-vs-buy-enterprise-software","Build vs Buy Enterprise Software: A Framework for the Decision",[214,2129,2130],{},[197,2131,2133],{"href":2132},"/blog/enterprise-data-management","Enterprise Data Management: Building the Single Source of Truth",[214,2135,2136],{},[197,2137,2139],{"href":2138},"/blog/enterprise-software-scalability","How to Design Enterprise Software That Scales With Your Business",[1309,2141,2142],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":239,"searchDepth":240,"depth":240,"links":2144},[2145,2146,2147,2148,2149,2150,2151,2152,2153],{"id":1355,"depth":243,"text":1356},{"id":1371,"depth":243,"text":1372},{"id":1402,"depth":243,"text":1403},{"id":1624,"depth":243,"text":1625},{"id":1795,"depth":243,"text":1796},{"id":1948,"depth":243,"text":1949},{"id":2063,"depth":243,"text":2064},{"id":2092,"depth":243,"text":2093},{"id":208,"depth":243,"text":209},"Engineering","API-first architecture treats integration as a first-class concern, not an afterthought. Here's how to design enterprise software that connects cleanly to everything it needs to.",[2157,2158],"api-first architecture","enterprise software architecture",{},"/blog/api-first-architecture",{"title":1349,"description":2155},"blog/api-first-architecture",[2164,1328,700,2165,2166],"API","Integration","Systems Design","9WP7FuJQaIW8iLqdv7uYkY49xwdrwUB-bz8-NJh7YsY",{"id":2169,"title":1289,"author":2170,"body":2171,"category":1328,"date":257,"description":2478,"extension":259,"featured":260,"image":261,"keywords":2479,"meta":2485,"navigation":266,"path":1288,"readTime":1338,"seo":2486,"stem":2487,"tags":2488,"__hash__":2491},"blog/blog/api-gateway-patterns.md",{"name":9,"bio":10},{"type":12,"value":2172,"toc":2463},[2173,2177,2180,2183,2186,2188,2192,2196,2211,2221,2225,2228,2231,2238,2245,2249,2252,2255,2258,2262,2265,2279,2282,2284,2288,2291,2294,2297,2317,2320,2323,2325,2329,2332,2335,2338,2344,2350,2353,2355,2359,2362,2365,2368,2370,2374,2377,2380,2386,2392,2398,2400,2404,2407,2413,2419,2425,2428,2430,2437,2439,2441],[15,2174,2176],{"id":2175},"more-than-a-traffic-cop","More Than a Traffic Cop",[20,2178,2179],{},"The typical description of an API gateway — a single entry point that routes requests to backend services — undersells what it actually does and undersells the architectural decisions involved in using one well.",[20,2181,2182],{},"An API gateway is a cross-cutting infrastructure component that handles capabilities every API needs but no single service should own: authentication verification, rate limiting, request transformation, response aggregation, logging, and caching. When you centralize these concerns at the gateway layer, individual services become simpler. When you do it wrong, the gateway becomes a bottleneck, a configuration nightmare, or a hidden point of coupling between your services.",[20,2184,2185],{},"Here's how to use API gateways effectively.",[30,2187],{},[15,2189,2191],{"id":2190},"the-core-responsibilities","The Core Responsibilities",[52,2193,2195],{"id":2194},"routing","Routing",[20,2197,2198,2199,2202,2203,2206,2207,2210],{},"The basic function: receive a request at ",[741,2200,2201],{},"/api/orders/123",", forward it to the ",[741,2204,2205],{},"OrderService"," at ",[741,2208,2209],{},"http://order-service:8080/orders/123",", and return the response. This is what people mean by \"reverse proxy\" — and it's the smallest part of what an API gateway should do.",[20,2212,2213,2214,2216,2217,2220],{},"What makes routing valuable in a gateway vs a plain reverse proxy: the gateway can route based on multiple criteria — path, query parameters, HTTP method, request headers, API version — and can perform transformations on the way in or out. It can rewrite paths (external ",[741,2215,800],{}," routes to internal ",[741,2218,2219],{},"/orders","), strip authentication headers before forwarding, or inject headers the downstream service expects.",[52,2222,2224],{"id":2223},"authentication-and-authorization","Authentication and Authorization",[20,2226,2227],{},"Every API needs authentication. But authentication is not a service-level concern — it's a cross-cutting concern. If authentication logic lives in every microservice, you've created an N-way coordination problem: every service needs the same auth library, every service needs to be updated when auth changes, every service adds latency for auth verification.",[20,2229,2230],{},"The gateway pattern: validate tokens at the gateway. If the token is invalid, reject the request before it ever reaches a service. If it's valid, inject the identity information as a trusted header that downstream services can use without re-verifying.",[20,2232,2233,2234,2237],{},"This means downstream services can trust ",[741,2235,2236],{},"X-User-ID: 12345"," because the gateway guarantees it only sets that header for authenticated requests. Services implement authorization (what this user can do) while the gateway handles authentication (who this user is).",[20,2239,2240,2241,2244],{},"The important caveat: services behind the gateway must be inaccessible from outside the network. If a client can reach ",[741,2242,2243],{},"order-service:8080"," directly and bypass the gateway, you've built security theater.",[52,2246,2248],{"id":2247},"rate-limiting","Rate Limiting",[20,2250,2251],{},"Rate limiting protects your services from being overwhelmed, prevents abuse, and implements fair-use policies. At the gateway, you can implement rate limiting per API key, per user, per IP, or per endpoint — with limits configurable centrally rather than duplicated across services.",[20,2253,2254],{},"The implementation complexity is in the strategy: does a rate limit reset per minute, per hour, per day? Do all servers share the same rate limit state (requires a shared cache like Redis) or do they each have their own (simpler but allows burst behavior)? What do you return when the limit is hit — a 429 with Retry-After header, or do you queue the request?",[20,2256,2257],{},"Gateway products like Kong, AWS API Gateway, and Traefik have rate limiting built in. If you're building a custom gateway, distributed rate limiting with Redis is the standard pattern.",[52,2259,2261],{"id":2260},"requestresponse-transformation","Request/Response Transformation",[20,2263,2264],{},"Gateways can modify requests and responses in flight. Common use cases:",[211,2266,2267,2270,2273,2276],{},[214,2268,2269],{},"Converting from one API format to another (SOAP to REST, XML to JSON)",[214,2271,2272],{},"Adding or removing headers",[214,2274,2275],{},"Transforming response structures to match what clients expect",[214,2277,2278],{},"Protocol translation (HTTP/1.1 to gRPC for backend services)",[20,2280,2281],{},"Use this carefully. Business logic does not belong in a gateway. Transformations at the gateway should be mechanical — format conversion, header manipulation — not semantic transformations that require understanding the domain.",[30,2283],{},[15,2285,2287],{"id":2286},"the-bff-pattern-backend-for-frontend","The BFF Pattern (Backend for Frontend)",[20,2289,2290],{},"The Backend for Frontend pattern is one of the most valuable API gateway patterns and one of the most underused.",[20,2292,2293],{},"The problem: a single generic API must serve multiple client types with different needs. A mobile client needs compact, battery-efficient responses with minimal data. A web dashboard needs rich, denormalized data for complex UI components. A third-party integration needs a stable, versioned API with predictable schemas. Trying to serve all three from a single API produces a bloated, compromised design that serves none of them well.",[20,2295,2296],{},"The BFF pattern creates a separate \"backend\" service (or gateway configuration) for each client type, each optimized for its consumer:",[211,2298,2299,2305,2311],{},[214,2300,2301,2304],{},[123,2302,2303],{},"Mobile BFF:"," Returns minimal payloads. Aggregates multiple service calls into single endpoints matching screen data requirements. Handles mobile-specific caching and offline concerns.",[214,2306,2307,2310],{},[123,2308,2309],{},"Web BFF:"," Returns richer data for complex UI components. Handles pagination, filtering, and search patterns appropriate for web interfaces.",[214,2312,2313,2316],{},[123,2314,2315],{},"Partner API:"," Provides a stable, versioned, well-documented API with conservative change management policies.",[20,2318,2319],{},"Each BFF owns the transformation and aggregation logic specific to its client, leaving the underlying services as clean domain-oriented APIs. Changes to the mobile client's data requirements modify the mobile BFF without affecting the web or partner APIs.",[20,2321,2322],{},"BFFs work best when they're owned by the same team as the client they serve. A mobile team owning the mobile BFF can evolve it as needed without coordinating with the team that owns the partner API.",[30,2324],{},[15,2326,2328],{"id":2327},"request-aggregation","Request Aggregation",[20,2330,2331],{},"Related to BFF but distinct: the gateway can aggregate multiple downstream requests into a single response.",[20,2333,2334],{},"A product detail page might need data from: the catalog service (product info), the inventory service (stock levels), the pricing service (current price and promotions), and the review service (ratings). Without aggregation, the client makes four requests serially or in parallel and assembles the data. With aggregation at the gateway (or BFF), the client makes one request and the backend handles the fan-out and assembly.",[20,2336,2337],{},"The trade-offs:",[20,2339,2340,2343],{},[123,2341,2342],{},"Benefits:"," Reduced client complexity, fewer network round trips for mobile clients, ability to parallelize backend calls server-side.",[20,2345,2346,2349],{},[123,2347,2348],{},"Costs:"," The gateway now needs domain knowledge to aggregate the responses. If the aggregation logic is complex, it belongs in a dedicated service (an API composition service or BFF), not in a generic gateway configuration.",[20,2351,2352],{},"The rule: simple aggregation (combine responses from three services into one object) is appropriate at the gateway. Complex aggregation (join data across services, apply business logic to the result) belongs in a dedicated service.",[30,2354],{},[15,2356,2358],{"id":2357},"caching","Caching",[20,2360,2361],{},"A gateway is a natural caching layer for responses that are expensive to compute and change infrequently. Product catalog data, public content, and configuration are good candidates. User-specific or frequently-updated data is not.",[20,2363,2364],{},"Gateway caching should respect Cache-Control headers from upstream services. Services declare their caching intentions; the gateway enforces them. Don't override a service's cache directives at the gateway — the service knows its data's freshness requirements better than the gateway does.",[20,2366,2367],{},"Cache invalidation at the gateway layer is particularly tricky. Design for this upfront rather than discovering the problem when you need to push an urgent update and your cache holds the old version.",[30,2369],{},[15,2371,2373],{"id":2372},"the-configuration-problem","The Configuration Problem",[20,2375,2376],{},"As API gateways grow, the configuration becomes a maintenance burden. Hundreds of route definitions, authentication rules, rate limit configurations, and transformation rules create a fragile configuration layer that's difficult to test and easy to break.",[20,2378,2379],{},"Strategies that help:",[20,2381,2382,2385],{},[123,2383,2384],{},"GitOps for gateway configuration."," Store gateway configuration in source control and apply it through CI/CD. Changes are reviewed, versioned, and auditable.",[20,2387,2388,2391],{},[123,2389,2390],{},"Service-owned routing."," In some gateway systems (Traefik, Kong with declarative config), services declare their own routing rules in configuration files deployed alongside the service. The gateway discovers and applies them. This distributes ownership of routing to the teams who own the services.",[20,2393,2394,2397],{},[123,2395,2396],{},"Testing gateway configuration."," Integration tests that verify routing, authentication enforcement, and rate limiting behavior should run in CI, not just be verified manually after deployment.",[30,2399],{},[15,2401,2403],{"id":2402},"choosing-a-gateway","Choosing a Gateway",[20,2405,2406],{},"The right gateway depends heavily on your context:",[20,2408,2409,2412],{},[123,2410,2411],{},"AWS API Gateway / Azure API Management / Google Cloud Endpoints:"," Appropriate for cloud-native deployments deeply integrated with the respective cloud's ecosystem. Good managed options if you don't want to operate gateway infrastructure.",[20,2414,2415,2418],{},[123,2416,2417],{},"Kong / Traefik / NGINX / Envoy:"," Self-hosted options with varying plugin ecosystems and configuration models. Kong and Traefik are particularly popular for microservices environments with plugin-based extensibility.",[20,2420,2421,2424],{},[123,2422,2423],{},"Custom BFF (Node.js/Go service):"," When a dedicated BFF pattern is the right answer and the aggregation logic is complex enough to warrant a real application.",[20,2426,2427],{},"The worst answer is \"we'll figure it out later.\" Authentication enforcement, rate limiting, and routing strategy are foundational. Get them right early.",[30,2429],{},[20,2431,2432,2433],{},"If you're designing an API gateway strategy or evaluating options for a microservices deployment, ",[197,2434,2436],{"href":199,"rel":2435},[201],"let's connect.",[30,2438],{},[15,2440,209],{"id":208},[211,2442,2443,2447,2453,2459],{},[214,2444,2445],{},[197,2446,704],{"href":1337},[214,2448,2449],{},[197,2450,2452],{"href":2451},"/blog/software-architecture-patterns","Software Architecture Patterns Every Architect Should Know",[214,2454,2455],{},[197,2456,2458],{"href":2457},"/blog/microservices-vs-monolith","Microservices vs Monolith: The Honest Trade-off Analysis",[214,2460,2461],{},[197,2462,1301],{"href":1300},{"title":239,"searchDepth":240,"depth":240,"links":2464},[2465,2466,2472,2473,2474,2475,2476,2477],{"id":2175,"depth":243,"text":2176},{"id":2190,"depth":243,"text":2191,"children":2467},[2468,2469,2470,2471],{"id":2194,"depth":240,"text":2195},{"id":2223,"depth":240,"text":2224},{"id":2247,"depth":240,"text":2248},{"id":2260,"depth":240,"text":2261},{"id":2286,"depth":243,"text":2287},{"id":2327,"depth":243,"text":2328},{"id":2357,"depth":243,"text":2358},{"id":2372,"depth":243,"text":2373},{"id":2402,"depth":243,"text":2403},{"id":208,"depth":243,"text":209},"API gateway patterns extend far beyond routing — authentication, rate limiting, aggregation, and the BFF pattern make gateways a critical architectural component. Here's how to use them effectively.",[2480,2481,2482,2483,2484],"api gateway patterns","API gateway architecture","BFF pattern","backend for frontend","API gateway vs reverse proxy",{},{"title":1289,"description":2478},"blog/api-gateway-patterns",[2489,1345,2490,1342],"API Gateway","Microservices","3yTQhEHcTr4fjzyT23gMwurV7uOsSnGRVZDBEaFL_Jo",{"id":2493,"title":2494,"author":2495,"body":2496,"category":2154,"date":257,"description":3470,"extension":259,"featured":260,"image":261,"keywords":3471,"meta":3474,"navigation":266,"path":3475,"readTime":921,"seo":3476,"stem":3477,"tags":3478,"__hash__":3482},"blog/blog/api-performance-optimization.md","API Performance Optimization: Making Your Endpoints Fast at Scale",{"name":9,"bio":10},{"type":12,"value":2497,"toc":3458},[2498,2502,2505,2508,2510,2514,2517,2522,2554,2557,2562,2703,2706,2708,2712,2715,2720,2843,2846,2851,2868,2870,2874,2877,2882,3030,3036,3042,3044,3048,3051,3057,3063,3077,3083,3085,3089,3095,3101,3114,3168,3171,3173,3177,3180,3282,3285,3287,3291,3297,3395,3401,3403,3407,3410,3413,3416,3418,3425,3427,3429,3455],[15,2499,2501],{"id":2500},"api-latency-is-a-product-problem","API Latency Is a Product Problem",[20,2503,2504],{},"Users experience your API through your UI. Every millisecond of API latency is a millisecond added to page loads, form submissions, and data refreshes. At scale, a 200ms median latency with a 2-second p99 means 1% of requests are failing your users significantly — and if you have 10,000 API calls per minute, that's 100 slow requests per minute.",[20,2506,2507],{},"This article walks through the systematic approach to measuring API performance, identifying the root causes of latency, and applying the optimizations that actually move the numbers.",[30,2509],{},[15,2511,2513],{"id":2512},"measuring-before-optimizing","Measuring Before Optimizing",[20,2515,2516],{},"You can't optimize what you don't measure, and you can't know if your optimization worked without before/after data.",[20,2518,2519],{},[123,2520,2521],{},"The metrics that matter:",[211,2523,2524,2530,2536,2542,2548],{},[214,2525,2526,2529],{},[123,2527,2528],{},"Median (p50) latency:"," The typical user experience. This is the number most monitoring dashboards show, and it's the least useful by itself.",[214,2531,2532,2535],{},[123,2533,2534],{},"p95 latency:"," 95% of requests are faster than this. The experience of most users who are having a \"bad\" day.",[214,2537,2538,2541],{},[123,2539,2540],{},"p99 latency:"," 99% of requests are faster than this. The tail latency. This is where real pain lives.",[214,2543,2544,2547],{},[123,2545,2546],{},"Error rate:"," The percentage of requests returning 4xx or 5xx responses.",[214,2549,2550,2553],{},[123,2551,2552],{},"Throughput:"," Requests per second. Rising throughput with rising latency indicates a scaling problem.",[20,2555,2556],{},"Median tells you the typical case. P99 tells you the worst case that users regularly encounter. Both matter; optimize p99 without letting median degrade.",[20,2558,2559],{},[123,2560,2561],{},"Instrumentation at the request level:",[734,2563,2565],{"className":1646,"code":2564,"language":1648,"meta":239,"style":239},"app.use(async (c, next) => {\n const start = Date.now()\n await next()\n const duration = Date.now() - start\n\n // Send to your observability platform\n metrics.histogram('api.request.duration', duration, {\n method: c.req.method,\n path: c.req.path,\n status: c.res.status.toString(),\n })\n})\n",[741,2566,2567,2597,2615,2624,2646,2651,2656,2672,2677,2682,2693,2698],{"__ignoreMap":239},[850,2568,2569,2572,2575,2577,2579,2581,2584,2586,2589,2592,2595],{"class":852,"line":853},[850,2570,2571],{"class":856},"app.",[850,2573,2574],{"class":1667},"use",[850,2576,1766],{"class":856},[850,2578,1661],{"class":1660},[850,2580,1123],{"class":856},[850,2582,2583],{"class":1676},"c",[850,2585,797],{"class":856},[850,2587,2588],{"class":1676},"next",[850,2590,2591],{"class":856},") ",[850,2593,2594],{"class":1660},"=>",[850,2596,1849],{"class":856},[850,2598,2599,2601,2604,2606,2609,2612],{"class":852,"line":243},[850,2600,1737],{"class":1660},[850,2602,2603],{"class":862}," start",[850,2605,1743],{"class":1660},[850,2607,2608],{"class":856}," Date.",[850,2610,2611],{"class":1667},"now",[850,2613,2614],{"class":856},"()\n",[850,2616,2617,2619,2622],{"class":852,"line":240},[850,2618,1746],{"class":1660},[850,2620,2621],{"class":1667}," next",[850,2623,2614],{"class":856},[850,2625,2626,2628,2631,2633,2635,2637,2640,2643],{"class":852,"line":884},[850,2627,1737],{"class":1660},[850,2629,2630],{"class":862}," duration",[850,2632,1743],{"class":1660},[850,2634,2608],{"class":856},[850,2636,2611],{"class":1667},[850,2638,2639],{"class":856},"() ",[850,2641,2642],{"class":1660},"-",[850,2644,2645],{"class":856}," start\n",[850,2647,2648],{"class":852,"line":897},[850,2649,2650],{"emptyLinePlaceholder":266},"\n",[850,2652,2653],{"class":852,"line":910},[850,2654,2655],{"class":1427}," // Send to your observability platform\n",[850,2657,2658,2661,2664,2666,2669],{"class":852,"line":921},[850,2659,2660],{"class":856}," metrics.",[850,2662,2663],{"class":1667},"histogram",[850,2665,1766],{"class":856},[850,2667,2668],{"class":877},"'api.request.duration'",[850,2670,2671],{"class":856},", duration, {\n",[850,2673,2674],{"class":852,"line":268},[850,2675,2676],{"class":856}," method: c.req.method,\n",[850,2678,2679],{"class":852,"line":1338},[850,2680,2681],{"class":856}," path: c.req.path,\n",[850,2683,2684,2687,2690],{"class":852,"line":1495},[850,2685,2686],{"class":856}," status: c.res.status.",[850,2688,2689],{"class":1667},"toString",[850,2691,2692],{"class":856},"(),\n",[850,2694,2695],{"class":852,"line":1503},[850,2696,2697],{"class":856}," })\n",[850,2699,2700],{"class":852,"line":1514},[850,2701,2702],{"class":856},"})\n",[20,2704,2705],{},"With per-route timing data in your observability platform, you can identify exactly which endpoints have high latency and tail latency problems.",[30,2707],{},[15,2709,2711],{"id":2710},"the-database-layer-usually-where-latency-lives","The Database Layer: Usually Where Latency Lives",[20,2713,2714],{},"As covered in the database performance article, the most common cause of slow API endpoints is slow database queries. The first step in diagnosing a slow endpoint is measuring query time versus total request time.",[20,2716,2717],{},[123,2718,2719],{},"Isolate the database time:",[734,2721,2723],{"className":1646,"code":2722,"language":1648,"meta":239,"style":239},"async function getProjectDetails(projectId: string) {\n const t0 = Date.now()\n const project = await db.project.findUnique({\n where: { id: projectId },\n include: { members: true, tasks: true, milestones: true }\n })\n metrics.histogram('db.query.project_details', Date.now() - t0)\n return project\n}\n",[741,2724,2725,2746,2761,2781,2786,2805,2809,2832,2839],{"__ignoreMap":239},[850,2726,2727,2729,2731,2734,2736,2739,2741,2743],{"class":852,"line":853},[850,2728,1661],{"class":1660},[850,2730,1664],{"class":1660},[850,2732,2733],{"class":1667}," getProjectDetails",[850,2735,1766],{"class":856},[850,2737,2738],{"class":1676},"projectId",[850,2740,1680],{"class":1660},[850,2742,1683],{"class":862},[850,2744,2745],{"class":856},") {\n",[850,2747,2748,2750,2753,2755,2757,2759],{"class":852,"line":243},[850,2749,1737],{"class":1660},[850,2751,2752],{"class":862}," t0",[850,2754,1743],{"class":1660},[850,2756,2608],{"class":856},[850,2758,2611],{"class":1667},[850,2760,2614],{"class":856},[850,2762,2763,2765,2768,2770,2772,2775,2778],{"class":852,"line":240},[850,2764,1737],{"class":1660},[850,2766,2767],{"class":862}," project",[850,2769,1743],{"class":1660},[850,2771,1746],{"class":1660},[850,2773,2774],{"class":856}," db.project.",[850,2776,2777],{"class":1667},"findUnique",[850,2779,2780],{"class":856},"({\n",[850,2782,2783],{"class":852,"line":884},[850,2784,2785],{"class":856}," where: { id: projectId },\n",[850,2787,2788,2791,2793,2796,2798,2801,2803],{"class":852,"line":897},[850,2789,2790],{"class":856}," include: { members: ",[850,2792,1097],{"class":862},[850,2794,2795],{"class":856},", tasks: ",[850,2797,1097],{"class":862},[850,2799,2800],{"class":856},", milestones: ",[850,2802,1097],{"class":862},[850,2804,924],{"class":856},[850,2806,2807],{"class":852,"line":910},[850,2808,2697],{"class":856},[850,2810,2811,2813,2815,2817,2820,2823,2825,2827,2829],{"class":852,"line":921},[850,2812,2660],{"class":856},[850,2814,2663],{"class":1667},[850,2816,1766],{"class":856},[850,2818,2819],{"class":877},"'db.query.project_details'",[850,2821,2822],{"class":856},", Date.",[850,2824,2611],{"class":1667},[850,2826,2639],{"class":856},[850,2828,2642],{"class":1660},[850,2830,2831],{"class":856}," t0)\n",[850,2833,2834,2836],{"class":852,"line":268},[850,2835,1757],{"class":1660},[850,2837,2838],{"class":856}," project\n",[850,2840,2841],{"class":852,"line":1338},[850,2842,929],{"class":856},[20,2844,2845],{},"If the database query takes 180ms out of a 200ms request, fixing the query solves 90% of the problem. If the query takes 10ms and the request takes 200ms, the problem is elsewhere.",[20,2847,2848],{},[123,2849,2850],{},"Query optimization quick wins:",[211,2852,2853,2856,2859,2865],{},[214,2854,2855],{},"Add indexes on columns used in WHERE, JOIN, and ORDER BY clauses",[214,2857,2858],{},"Replace N+1 patterns with eager loading or batch queries",[214,2860,2861,2862],{},"Select only the columns you need instead of ",[741,2863,2864],{},"SELECT *",[214,2866,2867],{},"Cache results for frequently-read, infrequently-changing data",[30,2869],{},[15,2871,2873],{"id":2872},"caching-at-the-api-layer","Caching at the API Layer",[20,2875,2876],{},"Application-level caching (Redis) reduces database load and request latency for read-heavy operations. The patterns that work:",[20,2878,2879],{},[123,2880,2881],{},"Response caching for public data:",[734,2883,2885],{"className":1646,"code":2884,"language":1648,"meta":239,"style":239},"async function getPublicProjectStats(projectId: string) {\n const cacheKey = `stats:${projectId}`\n const cached = await redis.get(cacheKey)\n if (cached) return JSON.parse(cached)\n\n const stats = await computeProjectStats(projectId)\n await redis.set(cacheKey, JSON.stringify(stats), 'EX', 300)\n return stats\n}\n",[741,2886,2887,2906,2923,2943,2965,2969,2986,3019,3026],{"__ignoreMap":239},[850,2888,2889,2891,2893,2896,2898,2900,2902,2904],{"class":852,"line":853},[850,2890,1661],{"class":1660},[850,2892,1664],{"class":1660},[850,2894,2895],{"class":1667}," getPublicProjectStats",[850,2897,1766],{"class":856},[850,2899,2738],{"class":1676},[850,2901,1680],{"class":1660},[850,2903,1683],{"class":862},[850,2905,2745],{"class":856},[850,2907,2908,2910,2913,2915,2918,2920],{"class":852,"line":243},[850,2909,1737],{"class":1660},[850,2911,2912],{"class":862}," cacheKey",[850,2914,1743],{"class":1660},[850,2916,2917],{"class":877}," `stats:${",[850,2919,2738],{"class":856},[850,2921,2922],{"class":877},"}`\n",[850,2924,2925,2927,2930,2932,2934,2937,2940],{"class":852,"line":240},[850,2926,1737],{"class":1660},[850,2928,2929],{"class":862}," cached",[850,2931,1743],{"class":1660},[850,2933,1746],{"class":1660},[850,2935,2936],{"class":856}," redis.",[850,2938,2939],{"class":1667},"get",[850,2941,2942],{"class":856},"(cacheKey)\n",[850,2944,2945,2948,2951,2954,2957,2959,2962],{"class":852,"line":884},[850,2946,2947],{"class":1660}," if",[850,2949,2950],{"class":856}," (cached) ",[850,2952,2953],{"class":1660},"return",[850,2955,2956],{"class":862}," JSON",[850,2958,778],{"class":856},[850,2960,2961],{"class":1667},"parse",[850,2963,2964],{"class":856},"(cached)\n",[850,2966,2967],{"class":852,"line":897},[850,2968,2650],{"emptyLinePlaceholder":266},[850,2970,2971,2973,2976,2978,2980,2983],{"class":852,"line":910},[850,2972,1737],{"class":1660},[850,2974,2975],{"class":862}," stats",[850,2977,1743],{"class":1660},[850,2979,1746],{"class":1660},[850,2981,2982],{"class":1667}," computeProjectStats",[850,2984,2985],{"class":856},"(projectId)\n",[850,2987,2988,2990,2992,2995,2998,3001,3003,3006,3009,3012,3014,3017],{"class":852,"line":921},[850,2989,1746],{"class":1660},[850,2991,2936],{"class":856},[850,2993,2994],{"class":1667},"set",[850,2996,2997],{"class":856},"(cacheKey, ",[850,2999,3000],{"class":862},"JSON",[850,3002,778],{"class":856},[850,3004,3005],{"class":1667},"stringify",[850,3007,3008],{"class":856},"(stats), ",[850,3010,3011],{"class":877},"'EX'",[850,3013,797],{"class":856},[850,3015,3016],{"class":862},"300",[850,3018,1772],{"class":856},[850,3020,3021,3023],{"class":852,"line":268},[850,3022,1757],{"class":1660},[850,3024,3025],{"class":856}," stats\n",[850,3027,3028],{"class":852,"line":1338},[850,3029,929],{"class":856},[20,3031,3032,3035],{},[123,3033,3034],{},"Cache warming for predictable access patterns:"," For dashboards that aggregate data (weekly summaries, report data), pre-compute and cache the results on a schedule rather than computing on-demand. The report runs at midnight; users get the cached result instantly.",[20,3037,3038,3041],{},[123,3039,3040],{},"Selective caching based on cache hit rates:"," Not all data is worth caching. Cache data that is expensive to compute (complex aggregations, multiple-table joins), accessed frequently (dashboard data, user preferences), and has low invalidation frequency. Skip caching for data that changes per-request or has complex invalidation logic.",[30,3043],{},[15,3045,3047],{"id":3046},"payload-size-and-serialization","Payload Size and Serialization",[20,3049,3050],{},"Large response payloads increase network transfer time and deserialization time on the client. Auditing what your API returns is often surprisingly productive.",[20,3052,3053,3056],{},[123,3054,3055],{},"Return only the fields the client needs."," If your user endpoint returns 30 fields but the client only uses 8, you're transferring and serializing 22 unnecessary fields on every call. GraphQL solves this structurally; with REST, use field selection parameters or create endpoint variants for different use cases.",[20,3058,3059,3062],{},[123,3060,3061],{},"Paginate large collections."," Returning 1,000 items in a single response is almost never correct. Add pagination to any endpoint that can return more than 100 items. Cursor-based pagination (returning a cursor for the next page rather than an offset) is more efficient for large datasets.",[20,3064,3065,3068,3069,3072,3073,3076],{},[123,3066,3067],{},"JSON serialization performance."," The standard ",[741,3070,3071],{},"JSON.stringify"," is slower than specialized serializers for high-volume endpoints. Libraries like ",[741,3074,3075],{},"fast-json-stringify"," (which pre-compiles serializers from a schema) are 2-5x faster for complex objects.",[20,3078,3079,3082],{},[123,3080,3081],{},"Compression."," Enable gzip or Brotli compression for responses. This is almost always a net win for text-based API responses over JSON — typical compression ratios are 70-80% for large JSON payloads. The CPU cost is low relative to the network transfer savings, especially for mobile clients on variable connections.",[30,3084],{},[15,3086,3088],{"id":3087},"connection-management","Connection Management",[20,3090,3091,3094],{},[123,3092,3093],{},"Database connection pooling."," Each database connection has overhead — memory on the database server, TCP connection setup cost, and in PostgreSQL, a dedicated process. Without connection pooling, every request creates and destroys a connection. With pooling, connections are reused.",[20,3096,3097,3098,778],{},"For PostgreSQL, use PgBouncer (external) or the connection pool built into Prisma. Configure the pool size based on your database server's max_connections setting and your application's concurrency — a common starting point is ",[741,3099,3100],{},"pool size = (number of CPU cores × 2) + spindle count",[20,3102,3103,3106,3107,3110,3111,1680],{},[123,3104,3105],{},"HTTP keep-alive for external API calls."," If your API makes HTTP requests to external services, use an HTTP client that maintains keep-alive connections rather than creating a new connection for each request. In Node.js, use an ",[741,3108,3109],{},"https.Agent"," with ",[741,3112,3113],{},"keepAlive: true",[734,3115,3117],{"className":1646,"code":3116,"language":1648,"meta":239,"style":239},"const agent = new https.Agent({ keepAlive: true, maxSockets: 100 })\nconst response = await fetch(url, { agent })\n",[741,3118,3119,3151],{"__ignoreMap":239},[850,3120,3121,3124,3127,3129,3132,3135,3138,3141,3143,3146,3149],{"class":852,"line":853},[850,3122,3123],{"class":1660},"const",[850,3125,3126],{"class":862}," agent",[850,3128,1743],{"class":1660},[850,3130,3131],{"class":1660}," new",[850,3133,3134],{"class":856}," https.",[850,3136,3137],{"class":1667},"Agent",[850,3139,3140],{"class":856},"({ keepAlive: ",[850,3142,1097],{"class":862},[850,3144,3145],{"class":856},", maxSockets: ",[850,3147,3148],{"class":862},"100",[850,3150,2697],{"class":856},[850,3152,3153,3155,3158,3160,3162,3165],{"class":852,"line":243},[850,3154,3123],{"class":1660},[850,3156,3157],{"class":862}," response",[850,3159,1743],{"class":1660},[850,3161,1746],{"class":1660},[850,3163,3164],{"class":1667}," fetch",[850,3166,3167],{"class":856},"(url, { agent })\n",[20,3169,3170],{},"This eliminates TCP handshake overhead for repeated calls to the same host.",[30,3172],{},[15,3174,3176],{"id":3175},"concurrency-dont-wait-when-you-dont-have-to","Concurrency: Don't Wait When You Don't Have To",[20,3178,3179],{},"APIs that make multiple independent requests sequentially waste time. If two operations don't depend on each other, run them in parallel.",[734,3181,3183],{"className":1646,"code":3182,"language":1648,"meta":239,"style":239},"// Sequential: 300ms if each takes 150ms\nconst user = await getUser(userId)\nconst stats = await getUserStats(userId)\n\n// Parallel: 150ms\nconst [user, stats] = await Promise.all([\n getUser(userId),\n getUserStats(userId),\n])\n",[741,3184,3185,3190,3207,3222,3226,3231,3264,3271,3277],{"__ignoreMap":239},[850,3186,3187],{"class":852,"line":853},[850,3188,3189],{"class":1427},"// Sequential: 300ms if each takes 150ms\n",[850,3191,3192,3194,3197,3199,3201,3204],{"class":852,"line":243},[850,3193,3123],{"class":1660},[850,3195,3196],{"class":862}," user",[850,3198,1743],{"class":1660},[850,3200,1746],{"class":1660},[850,3202,3203],{"class":1667}," getUser",[850,3205,3206],{"class":856},"(userId)\n",[850,3208,3209,3211,3213,3215,3217,3220],{"class":852,"line":240},[850,3210,3123],{"class":1660},[850,3212,2975],{"class":862},[850,3214,1743],{"class":1660},[850,3216,1746],{"class":1660},[850,3218,3219],{"class":1667}," getUserStats",[850,3221,3206],{"class":856},[850,3223,3224],{"class":852,"line":884},[850,3225,2650],{"emptyLinePlaceholder":266},[850,3227,3228],{"class":852,"line":897},[850,3229,3230],{"class":1427},"// Parallel: 150ms\n",[850,3232,3233,3235,3238,3241,3243,3246,3249,3252,3254,3256,3258,3261],{"class":852,"line":910},[850,3234,3123],{"class":1660},[850,3236,3237],{"class":856}," [",[850,3239,3240],{"class":862},"user",[850,3242,797],{"class":856},[850,3244,3245],{"class":862},"stats",[850,3247,3248],{"class":856},"] ",[850,3250,3251],{"class":1660},"=",[850,3253,1746],{"class":1660},[850,3255,1723],{"class":862},[850,3257,778],{"class":856},[850,3259,3260],{"class":1667},"all",[850,3262,3263],{"class":856},"([\n",[850,3265,3266,3268],{"class":852,"line":921},[850,3267,3203],{"class":1667},[850,3269,3270],{"class":856},"(userId),\n",[850,3272,3273,3275],{"class":852,"line":268},[850,3274,3219],{"class":1667},[850,3276,3270],{"class":856},[850,3278,3279],{"class":852,"line":1338},[850,3280,3281],{"class":856},"])\n",[20,3283,3284],{},"This pattern is particularly impactful for endpoints that aggregate data from multiple sources — user data, their recent activity, their team members, their account status — where each query is independent.",[30,3286],{},[15,3288,3290],{"id":3289},"rate-limiting-and-timeout-management","Rate Limiting and Timeout Management",[20,3292,3293,3296],{},[123,3294,3295],{},"Timeouts on everything."," Every external call your API makes — database queries, HTTP requests to third-party services, cache operations — should have a timeout. Without timeouts, a slow external service can hold your request threads indefinitely, causing cascading slowdowns.",[734,3298,3300],{"className":1646,"code":3299,"language":1648,"meta":239,"style":239},"const result = await Promise.race([\n fetchExternalData(id),\n new Promise((_, reject) =>\n setTimeout(() => reject(new Error('Timeout')), 5000)\n )\n])\n",[741,3301,3302,3322,3330,3352,3386,3391],{"__ignoreMap":239},[850,3303,3304,3306,3309,3311,3313,3315,3317,3320],{"class":852,"line":853},[850,3305,3123],{"class":1660},[850,3307,3308],{"class":862}," result",[850,3310,1743],{"class":1660},[850,3312,1746],{"class":1660},[850,3314,1723],{"class":862},[850,3316,778],{"class":856},[850,3318,3319],{"class":1667},"race",[850,3321,3263],{"class":856},[850,3323,3324,3327],{"class":852,"line":243},[850,3325,3326],{"class":1667}," fetchExternalData",[850,3328,3329],{"class":856},"(id),\n",[850,3331,3332,3334,3336,3339,3342,3344,3347,3349],{"class":852,"line":240},[850,3333,3131],{"class":1660},[850,3335,1723],{"class":862},[850,3337,3338],{"class":856},"((",[850,3340,3341],{"class":1676},"_",[850,3343,797],{"class":856},[850,3345,3346],{"class":1676},"reject",[850,3348,2591],{"class":856},[850,3350,3351],{"class":1660},"=>\n",[850,3353,3354,3357,3360,3362,3365,3367,3370,3373,3375,3378,3381,3384],{"class":852,"line":884},[850,3355,3356],{"class":1667}," setTimeout",[850,3358,3359],{"class":856},"(() ",[850,3361,2594],{"class":1660},[850,3363,3364],{"class":1667}," reject",[850,3366,1766],{"class":856},[850,3368,3369],{"class":1660},"new",[850,3371,3372],{"class":1667}," Error",[850,3374,1766],{"class":856},[850,3376,3377],{"class":877},"'Timeout'",[850,3379,3380],{"class":856},")), ",[850,3382,3383],{"class":862},"5000",[850,3385,1772],{"class":856},[850,3387,3388],{"class":852,"line":897},[850,3389,3390],{"class":856}," )\n",[850,3392,3393],{"class":852,"line":910},[850,3394,3281],{"class":856},[20,3396,3397,3400],{},[123,3398,3399],{},"Circuit breakers for external dependencies."," If a downstream service is consistently failing or slow, a circuit breaker prevents your API from waiting for it on every request. After a threshold of failures, the circuit \"opens\" and requests fail fast with a cached or degraded response until the downstream service recovers.",[30,3402],{},[15,3404,3406],{"id":3405},"load-testing-to-validate-improvements","Load Testing to Validate Improvements",[20,3408,3409],{},"Profiling individual queries and caching strategies improves latency in isolation. Load testing validates that your optimizations hold under real concurrency — multiple users hitting the same endpoints simultaneously.",[20,3411,3412],{},"Tools: k6 (JavaScript test scripts, good for CI integration), Artillery (YAML-based, easy to configure), Apache JMeter (UI-based, good for complex scenarios).",[20,3414,3415],{},"A basic load test protocol: establish baseline metrics at 10, 50, 100, and 500 concurrent users. Identify the concurrency level where latency starts to degrade significantly. Optimize, re-test, repeat.",[30,3417],{},[20,3419,3420,3421,778],{},"API performance optimization is a discipline, not a one-time task. Build the instrumentation, run it continuously, and treat regressions the same way you treat bugs. If you're working on an API with latency problems and want help diagnosing and prioritizing the work, book a call at ",[197,3422,3424],{"href":199,"rel":3423},[201],"calendly.com/jamesrossjr",[30,3426],{},[15,3428,209],{"id":208},[211,3430,3431,3437,3443,3449],{},[214,3432,3433],{},[197,3434,3436],{"href":3435},"/blog/nodejs-performance-optimization","Node.js Performance Optimization: The Practical Guide",[214,3438,3439],{},[197,3440,3442],{"href":3441},"/blog/api-rate-limiting","API Rate Limiting: Protecting Your Services Without Hurting Your Users",[214,3444,3445],{},[197,3446,3448],{"href":3447},"/blog/core-web-vitals-optimization","Core Web Vitals Optimization: A Developer's Complete Guide",[214,3450,3451],{},[197,3452,3454],{"href":3453},"/blog/database-indexing-strategies","Database Indexing Strategies That Actually Make Queries Fast",[1309,3456,3457],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":239,"searchDepth":240,"depth":240,"links":3459},[3460,3461,3462,3463,3464,3465,3466,3467,3468,3469],{"id":2500,"depth":243,"text":2501},{"id":2512,"depth":243,"text":2513},{"id":2710,"depth":243,"text":2711},{"id":2872,"depth":243,"text":2873},{"id":3046,"depth":243,"text":3047},{"id":3087,"depth":243,"text":3088},{"id":3175,"depth":243,"text":3176},{"id":3289,"depth":243,"text":3290},{"id":3405,"depth":243,"text":3406},{"id":208,"depth":243,"text":209},"Slow APIs kill user experience and increase infrastructure costs. Here's the systematic approach to profiling, optimizing, and scaling API performance in production.",[3472,3473],"API performance optimization","REST API performance",{},"/blog/api-performance-optimization",{"title":2494,"description":3470},"blog/api-performance-optimization",[3479,3480,3481],"API Performance","Performance","Backend","BWqBKqPyN_LzpW3pSi3awnfISFFidpHqWHymd3pQlik",{"id":3484,"title":3442,"author":3485,"body":3486,"category":2154,"date":257,"description":5850,"extension":259,"featured":260,"image":261,"keywords":5851,"meta":5854,"navigation":266,"path":3441,"readTime":921,"seo":5855,"stem":5856,"tags":5857,"__hash__":5859},"blog/blog/api-rate-limiting.md",{"name":9,"bio":10},{"type":12,"value":3487,"toc":5840},[3488,3491,3494,3498,3501,3507,3513,3519,3525,3528,3532,4306,4310,4313,4755,4758,5012,5016,5019,5363,5367,5370,5567,5571,5574,5765,5769,5775,5781,5791,5800,5803,5805,5811,5813,5815,5837],[20,3489,3490],{},"Rate limiting is one of those things you do not think about until you need it urgently — usually because something is hammering your API and degrading service for everyone. At that point, implementing it under pressure is much harder than having it in place from the start.",[20,3492,3493],{},"Good rate limiting protects your services while being nearly invisible to legitimate users. Bad rate limiting blocks legitimate users and fails to stop determined abusers.",[15,3495,3497],{"id":3496},"the-algorithms","The Algorithms",[20,3499,3500],{},"Three main algorithms are in common use:",[20,3502,3503,3506],{},[123,3504,3505],{},"Token Bucket:"," A bucket starts with N tokens. Each request consumes one token. Tokens are added at a fixed rate. Requests are rejected when the bucket is empty. Allows short bursts up to the bucket size, then throttles to the replenishment rate.",[20,3508,3509,3512],{},[123,3510,3511],{},"Fixed Window Counter:"," Count requests per fixed time window (e.g., 100 requests per minute, resetting at the top of each minute). Simple but has a burst vulnerability at the window boundary — a user can make 100 requests at 11:59:55 and 100 more at 12:00:05.",[20,3514,3515,3518],{},[123,3516,3517],{},"Sliding Window:"," Count requests in the last N seconds, using a sliding window. More accurate than fixed window but more complex to implement efficiently.",[20,3520,3521,3524],{},[123,3522,3523],{},"Sliding Window Log:"," Store the timestamp of each request. Count requests in the window by looking at the log. Most accurate but consumes more memory.",[20,3526,3527],{},"For most web APIs, a sliding window counter is the right balance: accurate enough to prevent the burst vulnerability, efficient enough to run at scale.",[15,3529,3531],{"id":3530},"redis-implementation","Redis Implementation",[734,3533,3535],{"className":1646,"code":3534,"language":1648,"meta":239,"style":239},"// lib/rateLimit.ts\nimport Redis from 'ioredis'\n\nInterface RateLimitResult {\n allowed: boolean\n limit: number\n remaining: number\n resetAt: Date\n retryAfter?: number // seconds until next request allowed\n}\n\nExport async function rateLimit(\n redis: Redis,\n identifier: string,\n config: {\n limit: number\n windowMs: number\n keyPrefix?: string\n }\n): Promise\u003CRateLimitResult> {\n const { limit, windowMs, keyPrefix = 'rl' } = config\n const now = Date.now()\n const windowStart = now - windowMs\n const key = `${keyPrefix}:${identifier}`\n\n const pipeline = redis.pipeline()\n\n // Remove entries outside the current window\n pipeline.zremrangebyscore(key, '-inf', windowStart)\n\n // Count current requests in window\n pipeline.zcard(key)\n\n // Add current request with timestamp as score\n pipeline.zadd(key, now, `${now}-${Math.random()}`)\n\n // Set expiry on the key\n pipeline.pexpire(key, windowMs)\n\n const results = await pipeline.exec()\n const count = (results?.[1]?.[1] as number) ?? 0\n\n const allowed = count \u003C limit\n const remaining = Math.max(0, limit - count - 1)\n\n if (!allowed) {\n // Calculate when the oldest request in the window expires\n const oldestEntry = await redis.zrange(key, 0, 0, 'WITHSCORES')\n const oldestTimestamp = Number(oldestEntry[1]) || now\n const resetAt = new Date(oldestTimestamp + windowMs)\n\n return {\n allowed: false,\n limit,\n remaining: 0,\n resetAt,\n retryAfter: Math.ceil((resetAt.getTime() - now) / 1000),\n }\n }\n\n return {\n allowed: true,\n limit,\n remaining,\n resetAt: new Date(now + windowMs),\n }\n}\n",[741,3536,3537,3542,3556,3560,3565,3573,3581,3588,3596,3609,3613,3617,3631,3643,3654,3663,3672,3681,3690,3694,3709,3742,3757,3774,3796,3801,3818,3823,3829,3847,3852,3858,3869,3874,3880,3915,3920,3926,3937,3942,3961,3998,4003,4020,4054,4059,4072,4078,4110,4137,4160,4165,4172,4183,4189,4199,4205,4236,4241,4246,4251,4258,4267,4272,4278,4296,4301],{"__ignoreMap":239},[850,3538,3539],{"class":852,"line":853},[850,3540,3541],{"class":1427},"// lib/rateLimit.ts\n",[850,3543,3544,3547,3550,3553],{"class":852,"line":243},[850,3545,3546],{"class":1660},"import",[850,3548,3549],{"class":856}," Redis ",[850,3551,3552],{"class":1660},"from",[850,3554,3555],{"class":877}," 'ioredis'\n",[850,3557,3558],{"class":852,"line":240},[850,3559,2650],{"emptyLinePlaceholder":266},[850,3561,3562],{"class":852,"line":884},[850,3563,3564],{"class":856},"Interface RateLimitResult {\n",[850,3566,3567,3570],{"class":852,"line":897},[850,3568,3569],{"class":1667}," allowed",[850,3571,3572],{"class":856},": boolean\n",[850,3574,3575,3578],{"class":852,"line":910},[850,3576,3577],{"class":1667}," limit",[850,3579,3580],{"class":856},": number\n",[850,3582,3583,3586],{"class":852,"line":921},[850,3584,3585],{"class":1667}," remaining",[850,3587,3580],{"class":856},[850,3589,3590,3593],{"class":852,"line":268},[850,3591,3592],{"class":1667}," resetAt",[850,3594,3595],{"class":856},": Date\n",[850,3597,3598,3601,3603,3606],{"class":852,"line":1338},[850,3599,3600],{"class":856}," retryAfter",[850,3602,1875],{"class":1660},[850,3604,3605],{"class":856}," number ",[850,3607,3608],{"class":1427},"// seconds until next request allowed\n",[850,3610,3611],{"class":852,"line":1495},[850,3612,929],{"class":856},[850,3614,3615],{"class":852,"line":1503},[850,3616,2650],{"emptyLinePlaceholder":266},[850,3618,3619,3622,3624,3626,3629],{"class":852,"line":1514},[850,3620,3621],{"class":856},"Export ",[850,3623,1661],{"class":1660},[850,3625,1664],{"class":1660},[850,3627,3628],{"class":1667}," rateLimit",[850,3630,1671],{"class":856},[850,3632,3633,3636,3638,3641],{"class":852,"line":1522},[850,3634,3635],{"class":1676}," redis",[850,3637,1680],{"class":1660},[850,3639,3640],{"class":1667}," Redis",[850,3642,881],{"class":856},[850,3644,3645,3648,3650,3652],{"class":852,"line":1530},[850,3646,3647],{"class":1676}," identifier",[850,3649,1680],{"class":1660},[850,3651,1683],{"class":862},[850,3653,881],{"class":856},[850,3655,3656,3659,3661],{"class":852,"line":1541},[850,3657,3658],{"class":1676}," config",[850,3660,1680],{"class":1660},[850,3662,1849],{"class":856},[850,3664,3665,3667,3669],{"class":852,"line":1548},[850,3666,3577],{"class":1676},[850,3668,1680],{"class":1660},[850,3670,3671],{"class":862}," number\n",[850,3673,3674,3677,3679],{"class":852,"line":1555},[850,3675,3676],{"class":1676}," windowMs",[850,3678,1680],{"class":1660},[850,3680,3671],{"class":862},[850,3682,3683,3686,3688],{"class":852,"line":1562},[850,3684,3685],{"class":1676}," keyPrefix",[850,3687,1875],{"class":1660},[850,3689,1713],{"class":862},[850,3691,3692],{"class":852,"line":1572},[850,3693,924],{"class":856},[850,3695,3696,3698,3700,3702,3704,3707],{"class":852,"line":1580},[850,3697,1718],{"class":856},[850,3699,1680],{"class":1660},[850,3701,1723],{"class":1667},[850,3703,1726],{"class":856},[850,3705,3706],{"class":1667},"RateLimitResult",[850,3708,1732],{"class":856},[850,3710,3711,3713,3716,3719,3721,3724,3726,3729,3731,3734,3737,3739],{"class":852,"line":1590},[850,3712,1737],{"class":1660},[850,3714,3715],{"class":856}," { ",[850,3717,3718],{"class":862},"limit",[850,3720,797],{"class":856},[850,3722,3723],{"class":862},"windowMs",[850,3725,797],{"class":856},[850,3727,3728],{"class":862},"keyPrefix",[850,3730,1743],{"class":1660},[850,3732,3733],{"class":877}," 'rl'",[850,3735,3736],{"class":856}," } ",[850,3738,3251],{"class":1660},[850,3740,3741],{"class":856}," config\n",[850,3743,3744,3746,3749,3751,3753,3755],{"class":852,"line":1597},[850,3745,1737],{"class":1660},[850,3747,3748],{"class":862}," now",[850,3750,1743],{"class":1660},[850,3752,2608],{"class":856},[850,3754,2611],{"class":1667},[850,3756,2614],{"class":856},[850,3758,3759,3761,3764,3766,3769,3771],{"class":852,"line":1604},[850,3760,1737],{"class":1660},[850,3762,3763],{"class":862}," windowStart",[850,3765,1743],{"class":1660},[850,3767,3768],{"class":856}," now ",[850,3770,2642],{"class":1660},[850,3772,3773],{"class":856}," windowMs\n",[850,3775,3776,3778,3781,3783,3786,3788,3791,3794],{"class":852,"line":1611},[850,3777,1737],{"class":1660},[850,3779,3780],{"class":862}," key",[850,3782,1743],{"class":1660},[850,3784,3785],{"class":877}," `${",[850,3787,3728],{"class":856},[850,3789,3790],{"class":877},"}:${",[850,3792,3793],{"class":856},"identifier",[850,3795,2922],{"class":877},[850,3797,3799],{"class":852,"line":3798},25,[850,3800,2650],{"emptyLinePlaceholder":266},[850,3802,3804,3806,3809,3811,3813,3816],{"class":852,"line":3803},26,[850,3805,1737],{"class":1660},[850,3807,3808],{"class":862}," pipeline",[850,3810,1743],{"class":1660},[850,3812,2936],{"class":856},[850,3814,3815],{"class":1667},"pipeline",[850,3817,2614],{"class":856},[850,3819,3821],{"class":852,"line":3820},27,[850,3822,2650],{"emptyLinePlaceholder":266},[850,3824,3826],{"class":852,"line":3825},28,[850,3827,3828],{"class":1427}," // Remove entries outside the current window\n",[850,3830,3832,3835,3838,3841,3844],{"class":852,"line":3831},29,[850,3833,3834],{"class":856}," pipeline.",[850,3836,3837],{"class":1667},"zremrangebyscore",[850,3839,3840],{"class":856},"(key, ",[850,3842,3843],{"class":877},"'-inf'",[850,3845,3846],{"class":856},", windowStart)\n",[850,3848,3850],{"class":852,"line":3849},30,[850,3851,2650],{"emptyLinePlaceholder":266},[850,3853,3855],{"class":852,"line":3854},31,[850,3856,3857],{"class":1427}," // Count current requests in window\n",[850,3859,3861,3863,3866],{"class":852,"line":3860},32,[850,3862,3834],{"class":856},[850,3864,3865],{"class":1667},"zcard",[850,3867,3868],{"class":856},"(key)\n",[850,3870,3872],{"class":852,"line":3871},33,[850,3873,2650],{"emptyLinePlaceholder":266},[850,3875,3877],{"class":852,"line":3876},34,[850,3878,3879],{"class":1427}," // Add current request with timestamp as score\n",[850,3881,3883,3885,3888,3891,3894,3896,3899,3902,3904,3907,3910,3913],{"class":852,"line":3882},35,[850,3884,3834],{"class":856},[850,3886,3887],{"class":1667},"zadd",[850,3889,3890],{"class":856},"(key, now, ",[850,3892,3893],{"class":877},"`${",[850,3895,2611],{"class":856},[850,3897,3898],{"class":877},"}-${",[850,3900,3901],{"class":856},"Math",[850,3903,778],{"class":877},[850,3905,3906],{"class":1667},"random",[850,3908,3909],{"class":877},"()",[850,3911,3912],{"class":877},"}`",[850,3914,1772],{"class":856},[850,3916,3918],{"class":852,"line":3917},36,[850,3919,2650],{"emptyLinePlaceholder":266},[850,3921,3923],{"class":852,"line":3922},37,[850,3924,3925],{"class":1427}," // Set expiry on the key\n",[850,3927,3929,3931,3934],{"class":852,"line":3928},38,[850,3930,3834],{"class":856},[850,3932,3933],{"class":1667},"pexpire",[850,3935,3936],{"class":856},"(key, windowMs)\n",[850,3938,3940],{"class":852,"line":3939},39,[850,3941,2650],{"emptyLinePlaceholder":266},[850,3943,3945,3947,3950,3952,3954,3956,3959],{"class":852,"line":3944},40,[850,3946,1737],{"class":1660},[850,3948,3949],{"class":862}," results",[850,3951,1743],{"class":1660},[850,3953,1746],{"class":1660},[850,3955,3834],{"class":856},[850,3957,3958],{"class":1667},"exec",[850,3960,2614],{"class":856},[850,3962,3964,3966,3969,3971,3974,3977,3980,3982,3984,3987,3990,3992,3995],{"class":852,"line":3963},41,[850,3965,1737],{"class":1660},[850,3967,3968],{"class":862}," count",[850,3970,1743],{"class":1660},[850,3972,3973],{"class":856}," (results?.[",[850,3975,3976],{"class":862},"1",[850,3978,3979],{"class":856},"]?.[",[850,3981,3976],{"class":862},[850,3983,3248],{"class":856},[850,3985,3986],{"class":1660},"as",[850,3988,3989],{"class":862}," number",[850,3991,2591],{"class":856},[850,3993,3994],{"class":1660},"??",[850,3996,3997],{"class":862}," 0\n",[850,3999,4001],{"class":852,"line":4000},42,[850,4002,2650],{"emptyLinePlaceholder":266},[850,4004,4006,4008,4010,4012,4015,4017],{"class":852,"line":4005},43,[850,4007,1737],{"class":1660},[850,4009,3569],{"class":862},[850,4011,1743],{"class":1660},[850,4013,4014],{"class":856}," count ",[850,4016,1726],{"class":1660},[850,4018,4019],{"class":856}," limit\n",[850,4021,4023,4025,4027,4029,4032,4035,4037,4040,4043,4045,4047,4049,4052],{"class":852,"line":4022},44,[850,4024,1737],{"class":1660},[850,4026,3585],{"class":862},[850,4028,1743],{"class":1660},[850,4030,4031],{"class":856}," Math.",[850,4033,4034],{"class":1667},"max",[850,4036,1766],{"class":856},[850,4038,4039],{"class":862},"0",[850,4041,4042],{"class":856},", limit ",[850,4044,2642],{"class":1660},[850,4046,4014],{"class":856},[850,4048,2642],{"class":1660},[850,4050,4051],{"class":862}," 1",[850,4053,1772],{"class":856},[850,4055,4057],{"class":852,"line":4056},45,[850,4058,2650],{"emptyLinePlaceholder":266},[850,4060,4062,4064,4066,4069],{"class":852,"line":4061},46,[850,4063,2947],{"class":1660},[850,4065,1123],{"class":856},[850,4067,4068],{"class":1660},"!",[850,4070,4071],{"class":856},"allowed) {\n",[850,4073,4075],{"class":852,"line":4074},47,[850,4076,4077],{"class":1427}," // Calculate when the oldest request in the window expires\n",[850,4079,4081,4083,4086,4088,4090,4092,4095,4097,4099,4101,4103,4105,4108],{"class":852,"line":4080},48,[850,4082,1737],{"class":1660},[850,4084,4085],{"class":862}," oldestEntry",[850,4087,1743],{"class":1660},[850,4089,1746],{"class":1660},[850,4091,2936],{"class":856},[850,4093,4094],{"class":1667},"zrange",[850,4096,3840],{"class":856},[850,4098,4039],{"class":862},[850,4100,797],{"class":856},[850,4102,4039],{"class":862},[850,4104,797],{"class":856},[850,4106,4107],{"class":877},"'WITHSCORES'",[850,4109,1772],{"class":856},[850,4111,4113,4115,4118,4120,4123,4126,4128,4131,4134],{"class":852,"line":4112},49,[850,4114,1737],{"class":1660},[850,4116,4117],{"class":862}," oldestTimestamp",[850,4119,1743],{"class":1660},[850,4121,4122],{"class":1667}," Number",[850,4124,4125],{"class":856},"(oldestEntry[",[850,4127,3976],{"class":862},[850,4129,4130],{"class":856},"]) ",[850,4132,4133],{"class":1660},"||",[850,4135,4136],{"class":856}," now\n",[850,4138,4140,4142,4144,4146,4148,4151,4154,4157],{"class":852,"line":4139},50,[850,4141,1737],{"class":1660},[850,4143,3592],{"class":862},[850,4145,1743],{"class":1660},[850,4147,3131],{"class":1660},[850,4149,4150],{"class":1667}," Date",[850,4152,4153],{"class":856},"(oldestTimestamp ",[850,4155,4156],{"class":1660},"+",[850,4158,4159],{"class":856}," windowMs)\n",[850,4161,4163],{"class":852,"line":4162},51,[850,4164,2650],{"emptyLinePlaceholder":266},[850,4166,4168,4170],{"class":852,"line":4167},52,[850,4169,1757],{"class":1660},[850,4171,1849],{"class":856},[850,4173,4175,4178,4181],{"class":852,"line":4174},53,[850,4176,4177],{"class":856}," allowed: ",[850,4179,4180],{"class":862},"false",[850,4182,881],{"class":856},[850,4184,4186],{"class":852,"line":4185},54,[850,4187,4188],{"class":856}," limit,\n",[850,4190,4192,4195,4197],{"class":852,"line":4191},55,[850,4193,4194],{"class":856}," remaining: ",[850,4196,4039],{"class":862},[850,4198,881],{"class":856},[850,4200,4202],{"class":852,"line":4201},56,[850,4203,4204],{"class":856}," resetAt,\n",[850,4206,4208,4211,4214,4217,4220,4222,4224,4227,4230,4233],{"class":852,"line":4207},57,[850,4209,4210],{"class":856}," retryAfter: Math.",[850,4212,4213],{"class":1667},"ceil",[850,4215,4216],{"class":856},"((resetAt.",[850,4218,4219],{"class":1667},"getTime",[850,4221,2639],{"class":856},[850,4223,2642],{"class":1660},[850,4225,4226],{"class":856}," now) ",[850,4228,4229],{"class":1660},"/",[850,4231,4232],{"class":862}," 1000",[850,4234,4235],{"class":856},"),\n",[850,4237,4239],{"class":852,"line":4238},58,[850,4240,924],{"class":856},[850,4242,4244],{"class":852,"line":4243},59,[850,4245,924],{"class":856},[850,4247,4249],{"class":852,"line":4248},60,[850,4250,2650],{"emptyLinePlaceholder":266},[850,4252,4254,4256],{"class":852,"line":4253},61,[850,4255,1757],{"class":1660},[850,4257,1849],{"class":856},[850,4259,4261,4263,4265],{"class":852,"line":4260},62,[850,4262,4177],{"class":856},[850,4264,1097],{"class":862},[850,4266,881],{"class":856},[850,4268,4270],{"class":852,"line":4269},63,[850,4271,4188],{"class":856},[850,4273,4275],{"class":852,"line":4274},64,[850,4276,4277],{"class":856}," remaining,\n",[850,4279,4281,4284,4286,4288,4291,4293],{"class":852,"line":4280},65,[850,4282,4283],{"class":856}," resetAt: ",[850,4285,3369],{"class":1660},[850,4287,4150],{"class":1667},[850,4289,4290],{"class":856},"(now ",[850,4292,4156],{"class":1660},[850,4294,4295],{"class":856}," windowMs),\n",[850,4297,4299],{"class":852,"line":4298},66,[850,4300,924],{"class":856},[850,4302,4304],{"class":852,"line":4303},67,[850,4305,929],{"class":856},[15,4307,4309],{"id":4308},"middleware-integration","Middleware Integration",[20,4311,4312],{},"Add rate limiting as middleware in your HTTP framework:",[734,4314,4316],{"className":1646,"code":4315,"language":1648,"meta":239,"style":239},"// middleware/rateLimit.ts (Hono)\nimport { createMiddleware } from 'hono/factory'\n\nExport function rateLimitMiddleware(\n config: {\n limit: number\n windowMs: number\n keyPrefix?: string\n keyGenerator?: (c: Context) => string\n onRejected?: (c: Context, result: RateLimitResult) => Response\n }\n) {\n return createMiddleware(async (c, next) => {\n const identifier = config.keyGenerator\n ? config.keyGenerator(c)\n : getRequestIP(c) ?? 'unknown'\n\n const result = await rateLimit(redis, identifier, config)\n\n // Always set rate limit headers\n c.header('X-RateLimit-Limit', String(config.limit))\n c.header('X-RateLimit-Remaining', String(result.remaining))\n c.header('X-RateLimit-Reset', String(Math.ceil(result.resetAt.getTime() / 1000)))\n\n if (!result.allowed) {\n c.header('Retry-After', String(result.retryAfter))\n\n if (config.onRejected) {\n return config.onRejected(c, result)\n }\n\n return c.json({\n error: {\n code: 'RATE_LIMITED',\n message: 'Too many requests. Please wait before retrying.',\n retryAfter: result.retryAfter,\n },\n }, 429)\n }\n\n await next()\n })\n}\n",[741,4317,4318,4323,4335,4339,4351,4359,4367,4375,4383,4405,4437,4441,4445,4470,4481,4495,4511,4515,4530,4534,4539,4560,4578,4612,4616,4627,4645,4649,4656,4668,4672,4676,4686,4691,4701,4711,4716,4721,4731,4735,4739,4747,4751],{"__ignoreMap":239},[850,4319,4320],{"class":852,"line":853},[850,4321,4322],{"class":1427},"// middleware/rateLimit.ts (Hono)\n",[850,4324,4325,4327,4330,4332],{"class":852,"line":243},[850,4326,3546],{"class":1660},[850,4328,4329],{"class":856}," { createMiddleware } ",[850,4331,3552],{"class":1660},[850,4333,4334],{"class":877}," 'hono/factory'\n",[850,4336,4337],{"class":852,"line":240},[850,4338,2650],{"emptyLinePlaceholder":266},[850,4340,4341,4343,4346,4349],{"class":852,"line":884},[850,4342,3621],{"class":856},[850,4344,4345],{"class":1660},"function",[850,4347,4348],{"class":1667}," rateLimitMiddleware",[850,4350,1671],{"class":856},[850,4352,4353,4355,4357],{"class":852,"line":897},[850,4354,3658],{"class":1676},[850,4356,1680],{"class":1660},[850,4358,1849],{"class":856},[850,4360,4361,4363,4365],{"class":852,"line":910},[850,4362,3577],{"class":1676},[850,4364,1680],{"class":1660},[850,4366,3671],{"class":862},[850,4368,4369,4371,4373],{"class":852,"line":921},[850,4370,3676],{"class":1676},[850,4372,1680],{"class":1660},[850,4374,3671],{"class":862},[850,4376,4377,4379,4381],{"class":852,"line":268},[850,4378,3685],{"class":1676},[850,4380,1875],{"class":1660},[850,4382,1713],{"class":862},[850,4384,4385,4388,4390,4392,4394,4396,4399,4401,4403],{"class":852,"line":1338},[850,4386,4387],{"class":1667}," keyGenerator",[850,4389,1875],{"class":1660},[850,4391,1123],{"class":856},[850,4393,2583],{"class":1676},[850,4395,1680],{"class":1660},[850,4397,4398],{"class":1667}," Context",[850,4400,2591],{"class":856},[850,4402,2594],{"class":1660},[850,4404,1713],{"class":862},[850,4406,4407,4410,4412,4414,4416,4418,4420,4422,4425,4427,4430,4432,4434],{"class":852,"line":1495},[850,4408,4409],{"class":1667}," onRejected",[850,4411,1875],{"class":1660},[850,4413,1123],{"class":856},[850,4415,2583],{"class":1676},[850,4417,1680],{"class":1660},[850,4419,4398],{"class":1667},[850,4421,797],{"class":856},[850,4423,4424],{"class":1676},"result",[850,4426,1680],{"class":1660},[850,4428,4429],{"class":1667}," RateLimitResult",[850,4431,2591],{"class":856},[850,4433,2594],{"class":1660},[850,4435,4436],{"class":1667}," Response\n",[850,4438,4439],{"class":852,"line":1503},[850,4440,924],{"class":856},[850,4442,4443],{"class":852,"line":1514},[850,4444,2745],{"class":856},[850,4446,4447,4449,4452,4454,4456,4458,4460,4462,4464,4466,4468],{"class":852,"line":1522},[850,4448,1757],{"class":1660},[850,4450,4451],{"class":1667}," createMiddleware",[850,4453,1766],{"class":856},[850,4455,1661],{"class":1660},[850,4457,1123],{"class":856},[850,4459,2583],{"class":1676},[850,4461,797],{"class":856},[850,4463,2588],{"class":1676},[850,4465,2591],{"class":856},[850,4467,2594],{"class":1660},[850,4469,1849],{"class":856},[850,4471,4472,4474,4476,4478],{"class":852,"line":1530},[850,4473,1737],{"class":1660},[850,4475,3647],{"class":862},[850,4477,1743],{"class":1660},[850,4479,4480],{"class":856}," config.keyGenerator\n",[850,4482,4483,4486,4489,4492],{"class":852,"line":1541},[850,4484,4485],{"class":1660}," ?",[850,4487,4488],{"class":856}," config.",[850,4490,4491],{"class":1667},"keyGenerator",[850,4493,4494],{"class":856},"(c)\n",[850,4496,4497,4500,4503,4506,4508],{"class":852,"line":1548},[850,4498,4499],{"class":1660}," :",[850,4501,4502],{"class":1667}," getRequestIP",[850,4504,4505],{"class":856},"(c) ",[850,4507,3994],{"class":1660},[850,4509,4510],{"class":877}," 'unknown'\n",[850,4512,4513],{"class":852,"line":1555},[850,4514,2650],{"emptyLinePlaceholder":266},[850,4516,4517,4519,4521,4523,4525,4527],{"class":852,"line":1562},[850,4518,1737],{"class":1660},[850,4520,3308],{"class":862},[850,4522,1743],{"class":1660},[850,4524,1746],{"class":1660},[850,4526,3628],{"class":1667},[850,4528,4529],{"class":856},"(redis, identifier, config)\n",[850,4531,4532],{"class":852,"line":1572},[850,4533,2650],{"emptyLinePlaceholder":266},[850,4535,4536],{"class":852,"line":1580},[850,4537,4538],{"class":1427}," // Always set rate limit headers\n",[850,4540,4541,4544,4547,4549,4552,4554,4557],{"class":852,"line":1590},[850,4542,4543],{"class":856}," c.",[850,4545,4546],{"class":1667},"header",[850,4548,1766],{"class":856},[850,4550,4551],{"class":877},"'X-RateLimit-Limit'",[850,4553,797],{"class":856},[850,4555,4556],{"class":1667},"String",[850,4558,4559],{"class":856},"(config.limit))\n",[850,4561,4562,4564,4566,4568,4571,4573,4575],{"class":852,"line":1597},[850,4563,4543],{"class":856},[850,4565,4546],{"class":1667},[850,4567,1766],{"class":856},[850,4569,4570],{"class":877},"'X-RateLimit-Remaining'",[850,4572,797],{"class":856},[850,4574,4556],{"class":1667},[850,4576,4577],{"class":856},"(result.remaining))\n",[850,4579,4580,4582,4584,4586,4589,4591,4593,4596,4598,4601,4603,4605,4607,4609],{"class":852,"line":1604},[850,4581,4543],{"class":856},[850,4583,4546],{"class":1667},[850,4585,1766],{"class":856},[850,4587,4588],{"class":877},"'X-RateLimit-Reset'",[850,4590,797],{"class":856},[850,4592,4556],{"class":1667},[850,4594,4595],{"class":856},"(Math.",[850,4597,4213],{"class":1667},[850,4599,4600],{"class":856},"(result.resetAt.",[850,4602,4219],{"class":1667},[850,4604,2639],{"class":856},[850,4606,4229],{"class":1660},[850,4608,4232],{"class":862},[850,4610,4611],{"class":856},")))\n",[850,4613,4614],{"class":852,"line":1611},[850,4615,2650],{"emptyLinePlaceholder":266},[850,4617,4618,4620,4622,4624],{"class":852,"line":3798},[850,4619,2947],{"class":1660},[850,4621,1123],{"class":856},[850,4623,4068],{"class":1660},[850,4625,4626],{"class":856},"result.allowed) {\n",[850,4628,4629,4631,4633,4635,4638,4640,4642],{"class":852,"line":3803},[850,4630,4543],{"class":856},[850,4632,4546],{"class":1667},[850,4634,1766],{"class":856},[850,4636,4637],{"class":877},"'Retry-After'",[850,4639,797],{"class":856},[850,4641,4556],{"class":1667},[850,4643,4644],{"class":856},"(result.retryAfter))\n",[850,4646,4647],{"class":852,"line":3820},[850,4648,2650],{"emptyLinePlaceholder":266},[850,4650,4651,4653],{"class":852,"line":3825},[850,4652,2947],{"class":1660},[850,4654,4655],{"class":856}," (config.onRejected) {\n",[850,4657,4658,4660,4662,4665],{"class":852,"line":3831},[850,4659,1757],{"class":1660},[850,4661,4488],{"class":856},[850,4663,4664],{"class":1667},"onRejected",[850,4666,4667],{"class":856},"(c, result)\n",[850,4669,4670],{"class":852,"line":3849},[850,4671,924],{"class":856},[850,4673,4674],{"class":852,"line":3854},[850,4675,2650],{"emptyLinePlaceholder":266},[850,4677,4678,4680,4682,4684],{"class":852,"line":3860},[850,4679,1757],{"class":1660},[850,4681,4543],{"class":856},[850,4683,846],{"class":1667},[850,4685,2780],{"class":856},[850,4687,4688],{"class":852,"line":3871},[850,4689,4690],{"class":856}," error: {\n",[850,4692,4693,4696,4699],{"class":852,"line":3876},[850,4694,4695],{"class":856}," code: ",[850,4697,4698],{"class":877},"'RATE_LIMITED'",[850,4700,881],{"class":856},[850,4702,4703,4706,4709],{"class":852,"line":3882},[850,4704,4705],{"class":856}," message: ",[850,4707,4708],{"class":877},"'Too many requests. Please wait before retrying.'",[850,4710,881],{"class":856},[850,4712,4713],{"class":852,"line":3917},[850,4714,4715],{"class":856}," retryAfter: result.retryAfter,\n",[850,4717,4718],{"class":852,"line":3922},[850,4719,4720],{"class":856}," },\n",[850,4722,4723,4726,4729],{"class":852,"line":3928},[850,4724,4725],{"class":856}," }, ",[850,4727,4728],{"class":862},"429",[850,4730,1772],{"class":856},[850,4732,4733],{"class":852,"line":3939},[850,4734,924],{"class":856},[850,4736,4737],{"class":852,"line":3944},[850,4738,2650],{"emptyLinePlaceholder":266},[850,4740,4741,4743,4745],{"class":852,"line":3963},[850,4742,1746],{"class":1660},[850,4744,2621],{"class":1667},[850,4746,2614],{"class":856},[850,4748,4749],{"class":852,"line":4000},[850,4750,2697],{"class":856},[850,4752,4753],{"class":852,"line":4005},[850,4754,929],{"class":856},[20,4756,4757],{},"Apply at different granularities:",[734,4759,4761],{"className":1646,"code":4760,"language":1648,"meta":239,"style":239},"// Global: 1000 requests per 15 minutes per IP\napp.use('*', rateLimitMiddleware({\n limit: 1000,\n windowMs: 15 * 60 * 1000,\n keyPrefix: 'global',\n}))\n\n// Auth endpoints: 10 attempts per 15 minutes\napp.use('/api/auth/*', rateLimitMiddleware({\n limit: 10,\n windowMs: 15 * 60 * 1000,\n keyPrefix: 'auth',\n}))\n\n// Authenticated users: by user ID instead of IP\napp.use('/api/*', rateLimitMiddleware({\n limit: 500,\n windowMs: 60 * 1000,\n keyPrefix: 'user',\n keyGenerator: (c) => {\n const userId = c.get('userId')\n return userId ?? getRequestIP(c) ?? 'unknown'\n },\n}))\n",[741,4762,4763,4768,4786,4796,4816,4826,4831,4835,4840,4857,4866,4882,4891,4895,4899,4904,4921,4930,4943,4952,4967,4987,5004,5008],{"__ignoreMap":239},[850,4764,4765],{"class":852,"line":853},[850,4766,4767],{"class":1427},"// Global: 1000 requests per 15 minutes per IP\n",[850,4769,4770,4772,4774,4776,4779,4781,4784],{"class":852,"line":243},[850,4771,2571],{"class":856},[850,4773,2574],{"class":1667},[850,4775,1766],{"class":856},[850,4777,4778],{"class":877},"'*'",[850,4780,797],{"class":856},[850,4782,4783],{"class":1667},"rateLimitMiddleware",[850,4785,2780],{"class":856},[850,4787,4788,4791,4794],{"class":852,"line":240},[850,4789,4790],{"class":856}," limit: ",[850,4792,4793],{"class":862},"1000",[850,4795,881],{"class":856},[850,4797,4798,4801,4804,4807,4810,4812,4814],{"class":852,"line":884},[850,4799,4800],{"class":856}," windowMs: ",[850,4802,4803],{"class":862},"15",[850,4805,4806],{"class":1660}," *",[850,4808,4809],{"class":862}," 60",[850,4811,4806],{"class":1660},[850,4813,4232],{"class":862},[850,4815,881],{"class":856},[850,4817,4818,4821,4824],{"class":852,"line":897},[850,4819,4820],{"class":856}," keyPrefix: ",[850,4822,4823],{"class":877},"'global'",[850,4825,881],{"class":856},[850,4827,4828],{"class":852,"line":910},[850,4829,4830],{"class":856},"}))\n",[850,4832,4833],{"class":852,"line":921},[850,4834,2650],{"emptyLinePlaceholder":266},[850,4836,4837],{"class":852,"line":268},[850,4838,4839],{"class":1427},"// Auth endpoints: 10 attempts per 15 minutes\n",[850,4841,4842,4844,4846,4848,4851,4853,4855],{"class":852,"line":1338},[850,4843,2571],{"class":856},[850,4845,2574],{"class":1667},[850,4847,1766],{"class":856},[850,4849,4850],{"class":877},"'/api/auth/*'",[850,4852,797],{"class":856},[850,4854,4783],{"class":1667},[850,4856,2780],{"class":856},[850,4858,4859,4861,4864],{"class":852,"line":1495},[850,4860,4790],{"class":856},[850,4862,4863],{"class":862},"10",[850,4865,881],{"class":856},[850,4867,4868,4870,4872,4874,4876,4878,4880],{"class":852,"line":1503},[850,4869,4800],{"class":856},[850,4871,4803],{"class":862},[850,4873,4806],{"class":1660},[850,4875,4809],{"class":862},[850,4877,4806],{"class":1660},[850,4879,4232],{"class":862},[850,4881,881],{"class":856},[850,4883,4884,4886,4889],{"class":852,"line":1514},[850,4885,4820],{"class":856},[850,4887,4888],{"class":877},"'auth'",[850,4890,881],{"class":856},[850,4892,4893],{"class":852,"line":1522},[850,4894,4830],{"class":856},[850,4896,4897],{"class":852,"line":1530},[850,4898,2650],{"emptyLinePlaceholder":266},[850,4900,4901],{"class":852,"line":1541},[850,4902,4903],{"class":1427},"// Authenticated users: by user ID instead of IP\n",[850,4905,4906,4908,4910,4912,4915,4917,4919],{"class":852,"line":1548},[850,4907,2571],{"class":856},[850,4909,2574],{"class":1667},[850,4911,1766],{"class":856},[850,4913,4914],{"class":877},"'/api/*'",[850,4916,797],{"class":856},[850,4918,4783],{"class":1667},[850,4920,2780],{"class":856},[850,4922,4923,4925,4928],{"class":852,"line":1555},[850,4924,4790],{"class":856},[850,4926,4927],{"class":862},"500",[850,4929,881],{"class":856},[850,4931,4932,4934,4937,4939,4941],{"class":852,"line":1562},[850,4933,4800],{"class":856},[850,4935,4936],{"class":862},"60",[850,4938,4806],{"class":1660},[850,4940,4232],{"class":862},[850,4942,881],{"class":856},[850,4944,4945,4947,4950],{"class":852,"line":1572},[850,4946,4820],{"class":856},[850,4948,4949],{"class":877},"'user'",[850,4951,881],{"class":856},[850,4953,4954,4956,4959,4961,4963,4965],{"class":852,"line":1580},[850,4955,4387],{"class":1667},[850,4957,4958],{"class":856},": (",[850,4960,2583],{"class":1676},[850,4962,2591],{"class":856},[850,4964,2594],{"class":1660},[850,4966,1849],{"class":856},[850,4968,4969,4971,4974,4976,4978,4980,4982,4985],{"class":852,"line":1590},[850,4970,1737],{"class":1660},[850,4972,4973],{"class":862}," userId",[850,4975,1743],{"class":1660},[850,4977,4543],{"class":856},[850,4979,2939],{"class":1667},[850,4981,1766],{"class":856},[850,4983,4984],{"class":877},"'userId'",[850,4986,1772],{"class":856},[850,4988,4989,4991,4994,4996,4998,5000,5002],{"class":852,"line":1597},[850,4990,1757],{"class":1660},[850,4992,4993],{"class":856}," userId ",[850,4995,3994],{"class":1660},[850,4997,4502],{"class":1667},[850,4999,4505],{"class":856},[850,5001,3994],{"class":1660},[850,5003,4510],{"class":877},[850,5005,5006],{"class":852,"line":1604},[850,5007,4720],{"class":856},[850,5009,5010],{"class":852,"line":1611},[850,5011,4830],{"class":856},[15,5013,5015],{"id":5014},"multi-tier-rate-limiting","Multi-Tier Rate Limiting",[20,5017,5018],{},"Different API consumers have different needs. Tiered limits give premium users more capacity without removing protection:",[734,5020,5022],{"className":1646,"code":5021,"language":1648,"meta":239,"style":239},"interface RateLimitTier {\n limit: number\n windowMs: number\n}\n\nConst tiers: Record\u003Cstring, RateLimitTier> = {\n free: { limit: 100, windowMs: 60 * 60 * 1000 }, // 100/hour\n pro: { limit: 1000, windowMs: 60 * 60 * 1000 }, // 1000/hour\n enterprise: { limit: 10000, windowMs: 60 * 60 * 1000 }, // 10000/hour\n}\n\nApp.use('/api/*', async (c, next) => {\n const apiKey = c.req.header('X-API-Key')\n const tier = apiKey ? await getTierForKey(apiKey) : 'free'\n const config = tiers[tier]\n\n const result = await rateLimit(redis, apiKey ?? getRequestIP(c)!, {\n ...config,\n keyPrefix: `tier:${tier}`,\n })\n\n if (!result.allowed) {\n return c.json({ error: 'Rate limit exceeded', tier }, 429)\n }\n\n await next()\n})\n",[741,5023,5024,5033,5041,5049,5053,5057,5080,5105,5129,5154,5158,5162,5191,5212,5240,5251,5255,5282,5290,5304,5308,5312,5322,5343,5347,5351,5359],{"__ignoreMap":239},[850,5025,5026,5028,5031],{"class":852,"line":853},[850,5027,1843],{"class":1660},[850,5029,5030],{"class":1667}," RateLimitTier",[850,5032,1849],{"class":856},[850,5034,5035,5037,5039],{"class":852,"line":243},[850,5036,3577],{"class":1676},[850,5038,1680],{"class":1660},[850,5040,3671],{"class":862},[850,5042,5043,5045,5047],{"class":852,"line":240},[850,5044,3676],{"class":1676},[850,5046,1680],{"class":1660},[850,5048,3671],{"class":862},[850,5050,5051],{"class":852,"line":884},[850,5052,929],{"class":856},[850,5054,5055],{"class":852,"line":897},[850,5056,2650],{"emptyLinePlaceholder":266},[850,5058,5059,5062,5065,5068,5070,5073,5076,5078],{"class":852,"line":910},[850,5060,5061],{"class":856},"Const ",[850,5063,5064],{"class":1667},"tiers",[850,5066,5067],{"class":856},": Record",[850,5069,1726],{"class":1660},[850,5071,5072],{"class":856},"string, RateLimitTier",[850,5074,5075],{"class":1660},">",[850,5077,1743],{"class":1660},[850,5079,1849],{"class":856},[850,5081,5082,5085,5087,5090,5092,5094,5096,5098,5100,5102],{"class":852,"line":921},[850,5083,5084],{"class":856}," free: { limit: ",[850,5086,3148],{"class":862},[850,5088,5089],{"class":856},", windowMs: ",[850,5091,4936],{"class":862},[850,5093,4806],{"class":1660},[850,5095,4809],{"class":862},[850,5097,4806],{"class":1660},[850,5099,4232],{"class":862},[850,5101,4725],{"class":856},[850,5103,5104],{"class":1427},"// 100/hour\n",[850,5106,5107,5110,5112,5114,5116,5118,5120,5122,5124,5126],{"class":852,"line":268},[850,5108,5109],{"class":856}," pro: { limit: ",[850,5111,4793],{"class":862},[850,5113,5089],{"class":856},[850,5115,4936],{"class":862},[850,5117,4806],{"class":1660},[850,5119,4809],{"class":862},[850,5121,4806],{"class":1660},[850,5123,4232],{"class":862},[850,5125,4725],{"class":856},[850,5127,5128],{"class":1427},"// 1000/hour\n",[850,5130,5131,5134,5137,5139,5141,5143,5145,5147,5149,5151],{"class":852,"line":1338},[850,5132,5133],{"class":856}," enterprise: { limit: ",[850,5135,5136],{"class":862},"10000",[850,5138,5089],{"class":856},[850,5140,4936],{"class":862},[850,5142,4806],{"class":1660},[850,5144,4809],{"class":862},[850,5146,4806],{"class":1660},[850,5148,4232],{"class":862},[850,5150,4725],{"class":856},[850,5152,5153],{"class":1427},"// 10000/hour\n",[850,5155,5156],{"class":852,"line":1495},[850,5157,929],{"class":856},[850,5159,5160],{"class":852,"line":1503},[850,5161,2650],{"emptyLinePlaceholder":266},[850,5163,5164,5167,5169,5171,5173,5175,5177,5179,5181,5183,5185,5187,5189],{"class":852,"line":1514},[850,5165,5166],{"class":856},"App.",[850,5168,2574],{"class":1667},[850,5170,1766],{"class":856},[850,5172,4914],{"class":877},[850,5174,797],{"class":856},[850,5176,1661],{"class":1660},[850,5178,1123],{"class":856},[850,5180,2583],{"class":1676},[850,5182,797],{"class":856},[850,5184,2588],{"class":1676},[850,5186,2591],{"class":856},[850,5188,2594],{"class":1660},[850,5190,1849],{"class":856},[850,5192,5193,5195,5198,5200,5203,5205,5207,5210],{"class":852,"line":1522},[850,5194,1737],{"class":1660},[850,5196,5197],{"class":862}," apiKey",[850,5199,1743],{"class":1660},[850,5201,5202],{"class":856}," c.req.",[850,5204,4546],{"class":1667},[850,5206,1766],{"class":856},[850,5208,5209],{"class":877},"'X-API-Key'",[850,5211,1772],{"class":856},[850,5213,5214,5216,5219,5221,5224,5227,5229,5232,5235,5237],{"class":852,"line":1530},[850,5215,1737],{"class":1660},[850,5217,5218],{"class":862}," tier",[850,5220,1743],{"class":1660},[850,5222,5223],{"class":856}," apiKey ",[850,5225,5226],{"class":1660},"?",[850,5228,1746],{"class":1660},[850,5230,5231],{"class":1667}," getTierForKey",[850,5233,5234],{"class":856},"(apiKey) ",[850,5236,1680],{"class":1660},[850,5238,5239],{"class":877}," 'free'\n",[850,5241,5242,5244,5246,5248],{"class":852,"line":1541},[850,5243,1737],{"class":1660},[850,5245,3658],{"class":862},[850,5247,1743],{"class":1660},[850,5249,5250],{"class":856}," tiers[tier]\n",[850,5252,5253],{"class":852,"line":1548},[850,5254,2650],{"emptyLinePlaceholder":266},[850,5256,5257,5259,5261,5263,5265,5267,5270,5272,5274,5277,5279],{"class":852,"line":1555},[850,5258,1737],{"class":1660},[850,5260,3308],{"class":862},[850,5262,1743],{"class":1660},[850,5264,1746],{"class":1660},[850,5266,3628],{"class":1667},[850,5268,5269],{"class":856},"(redis, apiKey ",[850,5271,3994],{"class":1660},[850,5273,4502],{"class":1667},[850,5275,5276],{"class":856},"(c)",[850,5278,4068],{"class":1660},[850,5280,5281],{"class":856},", {\n",[850,5283,5284,5287],{"class":852,"line":1562},[850,5285,5286],{"class":1660}," ...",[850,5288,5289],{"class":856},"config,\n",[850,5291,5292,5294,5297,5300,5302],{"class":852,"line":1572},[850,5293,4820],{"class":856},[850,5295,5296],{"class":877},"`tier:${",[850,5298,5299],{"class":856},"tier",[850,5301,3912],{"class":877},[850,5303,881],{"class":856},[850,5305,5306],{"class":852,"line":1580},[850,5307,2697],{"class":856},[850,5309,5310],{"class":852,"line":1590},[850,5311,2650],{"emptyLinePlaceholder":266},[850,5313,5314,5316,5318,5320],{"class":852,"line":1597},[850,5315,2947],{"class":1660},[850,5317,1123],{"class":856},[850,5319,4068],{"class":1660},[850,5321,4626],{"class":856},[850,5323,5324,5326,5328,5330,5333,5336,5339,5341],{"class":852,"line":1604},[850,5325,1757],{"class":1660},[850,5327,4543],{"class":856},[850,5329,846],{"class":1667},[850,5331,5332],{"class":856},"({ error: ",[850,5334,5335],{"class":877},"'Rate limit exceeded'",[850,5337,5338],{"class":856},", tier }, ",[850,5340,4728],{"class":862},[850,5342,1772],{"class":856},[850,5344,5345],{"class":852,"line":1611},[850,5346,924],{"class":856},[850,5348,5349],{"class":852,"line":3798},[850,5350,2650],{"emptyLinePlaceholder":266},[850,5352,5353,5355,5357],{"class":852,"line":3803},[850,5354,1746],{"class":1660},[850,5356,2621],{"class":1667},[850,5358,2614],{"class":856},[850,5360,5361],{"class":852,"line":3820},[850,5362,2702],{"class":856},[15,5364,5366],{"id":5365},"per-endpoint-limits","Per-Endpoint Limits",[20,5368,5369],{},"Some endpoints are more expensive than others. Apply different limits:",[734,5371,5373],{"className":1646,"code":5372,"language":1648,"meta":239,"style":239},"// Search is expensive — limit more aggressively\napp.get('/api/search', rateLimitMiddleware({\n limit: 30,\n windowMs: 60 * 1000, // 30 per minute\n keyPrefix: 'search',\n}), searchHandler)\n\n// Webhooks and mutations\napp.post('/api/webhooks', rateLimitMiddleware({\n limit: 5,\n windowMs: 60 * 1000, // 5 per minute\n keyPrefix: 'webhooks',\n}), webhookHandler)\n\n// AI/expensive operations\napp.post('/api/ai/generate', rateLimitMiddleware({\n limit: 10,\n windowMs: 60 * 60 * 1000, // 10 per hour\n keyPrefix: 'ai',\n}), aiHandler)\n",[741,5374,5375,5380,5397,5406,5421,5430,5435,5439,5444,5462,5471,5486,5495,5500,5504,5509,5526,5534,5553,5562],{"__ignoreMap":239},[850,5376,5377],{"class":852,"line":853},[850,5378,5379],{"class":1427},"// Search is expensive — limit more aggressively\n",[850,5381,5382,5384,5386,5388,5391,5393,5395],{"class":852,"line":243},[850,5383,2571],{"class":856},[850,5385,2939],{"class":1667},[850,5387,1766],{"class":856},[850,5389,5390],{"class":877},"'/api/search'",[850,5392,797],{"class":856},[850,5394,4783],{"class":1667},[850,5396,2780],{"class":856},[850,5398,5399,5401,5404],{"class":852,"line":240},[850,5400,4790],{"class":856},[850,5402,5403],{"class":862},"30",[850,5405,881],{"class":856},[850,5407,5408,5410,5412,5414,5416,5418],{"class":852,"line":884},[850,5409,4800],{"class":856},[850,5411,4936],{"class":862},[850,5413,4806],{"class":1660},[850,5415,4232],{"class":862},[850,5417,797],{"class":856},[850,5419,5420],{"class":1427},"// 30 per minute\n",[850,5422,5423,5425,5428],{"class":852,"line":897},[850,5424,4820],{"class":856},[850,5426,5427],{"class":877},"'search'",[850,5429,881],{"class":856},[850,5431,5432],{"class":852,"line":910},[850,5433,5434],{"class":856},"}), searchHandler)\n",[850,5436,5437],{"class":852,"line":921},[850,5438,2650],{"emptyLinePlaceholder":266},[850,5440,5441],{"class":852,"line":268},[850,5442,5443],{"class":1427},"// Webhooks and mutations\n",[850,5445,5446,5448,5451,5453,5456,5458,5460],{"class":852,"line":1338},[850,5447,2571],{"class":856},[850,5449,5450],{"class":1667},"post",[850,5452,1766],{"class":856},[850,5454,5455],{"class":877},"'/api/webhooks'",[850,5457,797],{"class":856},[850,5459,4783],{"class":1667},[850,5461,2780],{"class":856},[850,5463,5464,5466,5469],{"class":852,"line":1495},[850,5465,4790],{"class":856},[850,5467,5468],{"class":862},"5",[850,5470,881],{"class":856},[850,5472,5473,5475,5477,5479,5481,5483],{"class":852,"line":1503},[850,5474,4800],{"class":856},[850,5476,4936],{"class":862},[850,5478,4806],{"class":1660},[850,5480,4232],{"class":862},[850,5482,797],{"class":856},[850,5484,5485],{"class":1427},"// 5 per minute\n",[850,5487,5488,5490,5493],{"class":852,"line":1514},[850,5489,4820],{"class":856},[850,5491,5492],{"class":877},"'webhooks'",[850,5494,881],{"class":856},[850,5496,5497],{"class":852,"line":1522},[850,5498,5499],{"class":856},"}), webhookHandler)\n",[850,5501,5502],{"class":852,"line":1530},[850,5503,2650],{"emptyLinePlaceholder":266},[850,5505,5506],{"class":852,"line":1541},[850,5507,5508],{"class":1427},"// AI/expensive operations\n",[850,5510,5511,5513,5515,5517,5520,5522,5524],{"class":852,"line":1548},[850,5512,2571],{"class":856},[850,5514,5450],{"class":1667},[850,5516,1766],{"class":856},[850,5518,5519],{"class":877},"'/api/ai/generate'",[850,5521,797],{"class":856},[850,5523,4783],{"class":1667},[850,5525,2780],{"class":856},[850,5527,5528,5530,5532],{"class":852,"line":1555},[850,5529,4790],{"class":856},[850,5531,4863],{"class":862},[850,5533,881],{"class":856},[850,5535,5536,5538,5540,5542,5544,5546,5548,5550],{"class":852,"line":1562},[850,5537,4800],{"class":856},[850,5539,4936],{"class":862},[850,5541,4806],{"class":1660},[850,5543,4809],{"class":862},[850,5545,4806],{"class":1660},[850,5547,4232],{"class":862},[850,5549,797],{"class":856},[850,5551,5552],{"class":1427},"// 10 per hour\n",[850,5554,5555,5557,5560],{"class":852,"line":1572},[850,5556,4820],{"class":856},[850,5558,5559],{"class":877},"'ai'",[850,5561,881],{"class":856},[850,5563,5564],{"class":852,"line":1580},[850,5565,5566],{"class":856},"}), aiHandler)\n",[15,5568,5570],{"id":5569},"graceful-degradation","Graceful Degradation",[20,5572,5573],{},"Rate limiting should degrade gracefully when Redis is unavailable. Fail open (allow the request) rather than fail closed (block everything):",[734,5575,5577],{"className":1646,"code":5576,"language":1648,"meta":239,"style":239},"export async function rateLimitWithFallback(\n redis: Redis | null,\n identifier: string,\n config: RateLimitConfig\n): Promise\u003CRateLimitResult> {\n if (!redis) {\n // Redis unavailable: allow all requests, log the issue\n console.error('Rate limiter unavailable: Redis connection failed')\n return { allowed: true, limit: config.limit, remaining: config.limit, resetAt: new Date() }\n }\n\n try {\n return await rateLimit(redis, identifier, config)\n } catch (err) {\n console.error('Rate limit check failed:', err)\n return { allowed: true, limit: config.limit, remaining: config.limit, resetAt: new Date() }\n }\n}\n",[741,5578,5579,5594,5609,5619,5628,5642,5653,5658,5673,5692,5696,5700,5707,5717,5727,5741,5757,5761],{"__ignoreMap":239},[850,5580,5581,5584,5587,5589,5592],{"class":852,"line":853},[850,5582,5583],{"class":1660},"export",[850,5585,5586],{"class":1660}," async",[850,5588,1664],{"class":1660},[850,5590,5591],{"class":1667}," rateLimitWithFallback",[850,5593,1671],{"class":856},[850,5595,5596,5598,5600,5602,5604,5607],{"class":852,"line":243},[850,5597,3635],{"class":1676},[850,5599,1680],{"class":1660},[850,5601,3640],{"class":1667},[850,5603,1698],{"class":1660},[850,5605,5606],{"class":862}," null",[850,5608,881],{"class":856},[850,5610,5611,5613,5615,5617],{"class":852,"line":240},[850,5612,3647],{"class":1676},[850,5614,1680],{"class":1660},[850,5616,1683],{"class":862},[850,5618,881],{"class":856},[850,5620,5621,5623,5625],{"class":852,"line":884},[850,5622,3658],{"class":1676},[850,5624,1680],{"class":1660},[850,5626,5627],{"class":1667}," RateLimitConfig\n",[850,5629,5630,5632,5634,5636,5638,5640],{"class":852,"line":897},[850,5631,1718],{"class":856},[850,5633,1680],{"class":1660},[850,5635,1723],{"class":1667},[850,5637,1726],{"class":856},[850,5639,3706],{"class":1667},[850,5641,1732],{"class":856},[850,5643,5644,5646,5648,5650],{"class":852,"line":910},[850,5645,2947],{"class":1660},[850,5647,1123],{"class":856},[850,5649,4068],{"class":1660},[850,5651,5652],{"class":856},"redis) {\n",[850,5654,5655],{"class":852,"line":921},[850,5656,5657],{"class":1427}," // Redis unavailable: allow all requests, log the issue\n",[850,5659,5660,5663,5666,5668,5671],{"class":852,"line":268},[850,5661,5662],{"class":856}," console.",[850,5664,5665],{"class":1667},"error",[850,5667,1766],{"class":856},[850,5669,5670],{"class":877},"'Rate limiter unavailable: Redis connection failed'",[850,5672,1772],{"class":856},[850,5674,5675,5677,5680,5682,5685,5687,5689],{"class":852,"line":1338},[850,5676,1757],{"class":1660},[850,5678,5679],{"class":856}," { allowed: ",[850,5681,1097],{"class":862},[850,5683,5684],{"class":856},", limit: config.limit, remaining: config.limit, resetAt: ",[850,5686,3369],{"class":1660},[850,5688,4150],{"class":1667},[850,5690,5691],{"class":856},"() }\n",[850,5693,5694],{"class":852,"line":1495},[850,5695,924],{"class":856},[850,5697,5698],{"class":852,"line":1503},[850,5699,2650],{"emptyLinePlaceholder":266},[850,5701,5702,5705],{"class":852,"line":1514},[850,5703,5704],{"class":1660}," try",[850,5706,1849],{"class":856},[850,5708,5709,5711,5713,5715],{"class":852,"line":1522},[850,5710,1757],{"class":1660},[850,5712,1746],{"class":1660},[850,5714,3628],{"class":1667},[850,5716,4529],{"class":856},[850,5718,5719,5721,5724],{"class":852,"line":1530},[850,5720,3736],{"class":856},[850,5722,5723],{"class":1660},"catch",[850,5725,5726],{"class":856}," (err) {\n",[850,5728,5729,5731,5733,5735,5738],{"class":852,"line":1541},[850,5730,5662],{"class":856},[850,5732,5665],{"class":1667},[850,5734,1766],{"class":856},[850,5736,5737],{"class":877},"'Rate limit check failed:'",[850,5739,5740],{"class":856},", err)\n",[850,5742,5743,5745,5747,5749,5751,5753,5755],{"class":852,"line":1548},[850,5744,1757],{"class":1660},[850,5746,5679],{"class":856},[850,5748,1097],{"class":862},[850,5750,5684],{"class":856},[850,5752,3369],{"class":1660},[850,5754,4150],{"class":1667},[850,5756,5691],{"class":856},[850,5758,5759],{"class":852,"line":1555},[850,5760,924],{"class":856},[850,5762,5763],{"class":852,"line":1562},[850,5764,929],{"class":856},[15,5766,5768],{"id":5767},"avoiding-common-mistakes","Avoiding Common Mistakes",[20,5770,5771,5774],{},[123,5772,5773],{},"Using IP address as the only identifier."," Many legitimate users share IPs (corporate NATs, VPN services). Rate limiting by IP alone is blunt. Rate limit by user ID for authenticated endpoints and use IP only as a fallback.",[20,5776,5777,5780],{},[123,5778,5779],{},"Setting limits too low."," If your legitimate users frequently hit the limit, the limit is wrong. Check your rate limit hit logs — if mostly legitimate users are getting 429s, raise the limit.",[20,5782,5783,5786,5787,5790],{},[123,5784,5785],{},"Not communicating the limit to clients."," Return ",[741,5788,5789],{},"X-RateLimit-*"," headers on every response, not just 429s. Well-implemented API clients use these headers to throttle themselves and never hit the limit.",[20,5792,5793,5796,5797,5799],{},[123,5794,5795],{},"No retry-after header on 429."," When you return 429, always include ",[741,5798,1255],{},". Clients that respect this stop hammering immediately and retry at the right time. Clients that do not get this header just keep retrying, making the problem worse.",[20,5801,5802],{},"Rate limiting is a protection mechanism and a service quality guarantee. Legitimate users should never notice it. Abusive requests should be stopped cleanly and quickly.",[30,5804],{},[20,5806,5807,5808,778],{},"Implementing rate limiting for an API or dealing with traffic abuse issues? I can help design a strategy that protects without frustrating legitimate users. Book a call: ",[197,5809,3424],{"href":199,"rel":5810},[201],[30,5812],{},[15,5814,209],{"id":208},[211,5816,5817,5821,5825,5831],{},[214,5818,5819],{},[197,5820,2494],{"href":3475},[214,5822,5823],{},[197,5824,1349],{"href":2160},[214,5826,5827],{},[197,5828,5830],{"href":5829},"/blog/nuxt-api-routes-nitro","Nuxt API Routes With Nitro: Building Your Backend in the Same Repo",[214,5832,5833],{},[197,5834,5836],{"href":5835},"/blog/nuxt-authentication-guide","Authentication in Nuxt: Patterns That Actually Scale",[1309,5838,5839],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":239,"searchDepth":240,"depth":240,"links":5841},[5842,5843,5844,5845,5846,5847,5848,5849],{"id":3496,"depth":243,"text":3497},{"id":3530,"depth":243,"text":3531},{"id":4308,"depth":243,"text":4309},{"id":5014,"depth":243,"text":5015},{"id":5365,"depth":243,"text":5366},{"id":5569,"depth":243,"text":5570},{"id":5767,"depth":243,"text":5768},{"id":208,"depth":243,"text":209},"A complete guide to API rate limiting — algorithms, Redis implementation, per-endpoint limits, rate limit headers, graceful degradation, and strategies that protect without frustrating legitimate users.",[5852,5853],"API rate limiting","API protection",{},{"title":3442,"description":5850},"blog/api-rate-limiting",[2164,5858,3481],"Security","faj_mwhEkNeRA51uX0UiJadd6K3iE5ODeLKmaNiRMzc",{"id":5861,"title":5862,"author":5863,"body":5864,"category":5858,"date":257,"description":7727,"extension":259,"featured":260,"image":261,"keywords":7728,"meta":7731,"navigation":266,"path":7732,"readTime":921,"seo":7733,"stem":7734,"tags":7735,"__hash__":7738},"blog/blog/api-security-best-practices.md","API Security Best Practices: Protecting Your Endpoints in Production",{"name":9,"bio":10},{"type":12,"value":5865,"toc":7716},[5866,5870,5873,5876,5880,5883,5889,5895,5898,5901,6151,6154,6158,6161,6164,6306,6309,6477,6479,6482,6496,6499,6713,6716,6720,6723,7027,7030,7034,7037,7040,7216,7219,7223,7226,7229,7367,7378,7382,7385,7405,7518,7521,7525,7528,7672,7675,7677,7683,7685,7687,7713],[5867,5868,5862],"h1",{"id":5869},"api-security-best-practices-protecting-your-endpoints-in-production",[20,5871,5872],{},"APIs are the attack surface of modern applications. Your frontend is largely irrelevant from a security perspective — a determined attacker ignores your UI and talks directly to your API endpoints. Every input validation you do only in JavaScript is bypassed. Every \"hidden\" endpoint that your UI does not display is still accessible. Every endpoint that does not check authorization is exploitable.",[20,5874,5875],{},"Building secure APIs requires thinking like someone who will never see your frontend. Here is what that looks like in practice.",[15,5877,5879],{"id":5878},"authentication-knowing-who-is-calling","Authentication: Knowing Who Is Calling",[20,5881,5882],{},"Every non-public API endpoint must verify the caller's identity before processing the request. The two dominant patterns are JWT (JSON Web Tokens) and session-based authentication. They have different tradeoffs.",[20,5884,5885,5888],{},[123,5886,5887],{},"JWT"," — the caller presents a signed token with claims embedded. The server validates the signature and trusts the claims without a database lookup. This is stateless and scales well. The downside: JWTs cannot be invalidated before expiry without a blacklist (which adds the database lookup you were trying to avoid). Use short expiry times (15 minutes) with refresh tokens.",[20,5890,5891,5894],{},[123,5892,5893],{},"Session-based"," — the server issues a session ID stored in a cookie. Every request looks up the session in a database or cache. This allows instant session invalidation (logout destroys the session). The downside: every request requires a database/cache lookup.",[20,5896,5897],{},"For most applications, session-based authentication is simpler and more secure (because logout actually works). JWTs are appropriate when you need stateless horizontal scaling or when you are issuing tokens for third-party API access.",[20,5899,5900],{},"Regardless of which you use:",[734,5902,5904],{"className":1646,"code":5903,"language":1648,"meta":239,"style":239},"// Middleware that authenticates every protected route\nexport async function authenticate(\n req: Request,\n res: Response,\n next: NextFunction\n): Promise\u003Cvoid> {\n const token = req.headers.authorization?.replace(\"Bearer \", \"\");\n\n if (!token) {\n res.status(401).json({ error: \"Authentication required\" });\n return;\n }\n\n try {\n const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;\n req.user = { id: payload.sub!, email: payload.email };\n next();\n } catch {\n res.status(401).json({ error: \"Invalid or expired token\" });\n }\n}\n",[741,5905,5906,5911,5924,5936,5948,5957,5972,6000,6004,6015,6040,6046,6050,6054,6060,6092,6107,6114,6122,6143,6147],{"__ignoreMap":239},[850,5907,5908],{"class":852,"line":853},[850,5909,5910],{"class":1427},"// Middleware that authenticates every protected route\n",[850,5912,5913,5915,5917,5919,5922],{"class":852,"line":243},[850,5914,5583],{"class":1660},[850,5916,5586],{"class":1660},[850,5918,1664],{"class":1660},[850,5920,5921],{"class":1667}," authenticate",[850,5923,1671],{"class":856},[850,5925,5926,5929,5931,5934],{"class":852,"line":240},[850,5927,5928],{"class":1676}," req",[850,5930,1680],{"class":1660},[850,5932,5933],{"class":1667}," Request",[850,5935,881],{"class":856},[850,5937,5938,5941,5943,5946],{"class":852,"line":884},[850,5939,5940],{"class":1676}," res",[850,5942,1680],{"class":1660},[850,5944,5945],{"class":1667}," Response",[850,5947,881],{"class":856},[850,5949,5950,5952,5954],{"class":852,"line":897},[850,5951,2621],{"class":1676},[850,5953,1680],{"class":1660},[850,5955,5956],{"class":1667}," NextFunction\n",[850,5958,5959,5961,5963,5965,5967,5970],{"class":852,"line":910},[850,5960,1718],{"class":856},[850,5962,1680],{"class":1660},[850,5964,1723],{"class":1667},[850,5966,1726],{"class":856},[850,5968,5969],{"class":862},"void",[850,5971,1732],{"class":856},[850,5973,5974,5976,5979,5981,5984,5987,5989,5992,5994,5997],{"class":852,"line":921},[850,5975,1737],{"class":1660},[850,5977,5978],{"class":862}," token",[850,5980,1743],{"class":1660},[850,5982,5983],{"class":856}," req.headers.authorization?.",[850,5985,5986],{"class":1667},"replace",[850,5988,1766],{"class":856},[850,5990,5991],{"class":877},"\"Bearer \"",[850,5993,797],{"class":856},[850,5995,5996],{"class":877},"\"\"",[850,5998,5999],{"class":856},");\n",[850,6001,6002],{"class":852,"line":268},[850,6003,2650],{"emptyLinePlaceholder":266},[850,6005,6006,6008,6010,6012],{"class":852,"line":1338},[850,6007,2947],{"class":1660},[850,6009,1123],{"class":856},[850,6011,4068],{"class":1660},[850,6013,6014],{"class":856},"token) {\n",[850,6016,6017,6020,6023,6025,6027,6030,6032,6034,6037],{"class":852,"line":1495},[850,6018,6019],{"class":856}," res.",[850,6021,6022],{"class":1667},"status",[850,6024,1766],{"class":856},[850,6026,1181],{"class":862},[850,6028,6029],{"class":856},").",[850,6031,846],{"class":1667},[850,6033,5332],{"class":856},[850,6035,6036],{"class":877},"\"Authentication required\"",[850,6038,6039],{"class":856}," });\n",[850,6041,6042,6044],{"class":852,"line":1503},[850,6043,1757],{"class":1660},[850,6045,1896],{"class":856},[850,6047,6048],{"class":852,"line":1514},[850,6049,924],{"class":856},[850,6051,6052],{"class":852,"line":1522},[850,6053,2650],{"emptyLinePlaceholder":266},[850,6055,6056,6058],{"class":852,"line":1530},[850,6057,5704],{"class":1660},[850,6059,1849],{"class":856},[850,6061,6062,6064,6067,6069,6072,6075,6078,6081,6083,6085,6087,6090],{"class":852,"line":1541},[850,6063,1737],{"class":1660},[850,6065,6066],{"class":862}," payload",[850,6068,1743],{"class":1660},[850,6070,6071],{"class":856}," jwt.",[850,6073,6074],{"class":1667},"verify",[850,6076,6077],{"class":856},"(token, process.env.",[850,6079,6080],{"class":862},"JWT_SECRET",[850,6082,4068],{"class":1660},[850,6084,2591],{"class":856},[850,6086,3986],{"class":1660},[850,6088,6089],{"class":1667}," JwtPayload",[850,6091,1896],{"class":856},[850,6093,6094,6097,6099,6102,6104],{"class":852,"line":1548},[850,6095,6096],{"class":856}," req.user ",[850,6098,3251],{"class":1660},[850,6100,6101],{"class":856}," { id: payload.sub",[850,6103,4068],{"class":1660},[850,6105,6106],{"class":856},", email: payload.email };\n",[850,6108,6109,6111],{"class":852,"line":1555},[850,6110,2621],{"class":1667},[850,6112,6113],{"class":856},"();\n",[850,6115,6116,6118,6120],{"class":852,"line":1562},[850,6117,3736],{"class":856},[850,6119,5723],{"class":1660},[850,6121,1849],{"class":856},[850,6123,6124,6126,6128,6130,6132,6134,6136,6138,6141],{"class":852,"line":1572},[850,6125,6019],{"class":856},[850,6127,6022],{"class":1667},[850,6129,1766],{"class":856},[850,6131,1181],{"class":862},[850,6133,6029],{"class":856},[850,6135,846],{"class":1667},[850,6137,5332],{"class":856},[850,6139,6140],{"class":877},"\"Invalid or expired token\"",[850,6142,6039],{"class":856},[850,6144,6145],{"class":852,"line":1580},[850,6146,924],{"class":856},[850,6148,6149],{"class":852,"line":1590},[850,6150,929],{"class":856},[20,6152,6153],{},"Never put authentication in the frontend route matching logic — put it in middleware that runs on every protected endpoint, before any request processing.",[15,6155,6157],{"id":6156},"authorization-what-callers-can-do","Authorization: What Callers Can Do",[20,6159,6160],{},"Authentication establishes identity. Authorization determines what that identity can access. These are separate concerns. A common mistake is to conflate them: \"the user is authenticated, so they can access anything.\"",[20,6162,6163],{},"Authorization must be enforced at the data layer, not just the route level. Every database query for user-specific data must filter by the authenticated user's context:",[734,6165,6167],{"className":1646,"code":6166,"language":1648,"meta":239,"style":239},"// Broken access control — returns any resource by ID\nasync function getDocument(id: string) {\n return db.document.findById(id);\n}\n\n// Correct — enforces ownership\nasync function getDocument(id: string, userId: string) {\n const doc = await db.document.findFirst({\n where: { id, userId },\n });\n if (!doc) throw new NotFoundError();\n return doc;\n}\n",[741,6168,6169,6174,6194,6207,6211,6215,6220,6247,6265,6270,6274,6295,6302],{"__ignoreMap":239},[850,6170,6171],{"class":852,"line":853},[850,6172,6173],{"class":1427},"// Broken access control — returns any resource by ID\n",[850,6175,6176,6178,6180,6183,6185,6188,6190,6192],{"class":852,"line":243},[850,6177,1661],{"class":1660},[850,6179,1664],{"class":1660},[850,6181,6182],{"class":1667}," getDocument",[850,6184,1766],{"class":856},[850,6186,6187],{"class":1676},"id",[850,6189,1680],{"class":1660},[850,6191,1683],{"class":862},[850,6193,2745],{"class":856},[850,6195,6196,6198,6201,6204],{"class":852,"line":240},[850,6197,1757],{"class":1660},[850,6199,6200],{"class":856}," db.document.",[850,6202,6203],{"class":1667},"findById",[850,6205,6206],{"class":856},"(id);\n",[850,6208,6209],{"class":852,"line":884},[850,6210,929],{"class":856},[850,6212,6213],{"class":852,"line":897},[850,6214,2650],{"emptyLinePlaceholder":266},[850,6216,6217],{"class":852,"line":910},[850,6218,6219],{"class":1427},"// Correct — enforces ownership\n",[850,6221,6222,6224,6226,6228,6230,6232,6234,6236,6238,6241,6243,6245],{"class":852,"line":921},[850,6223,1661],{"class":1660},[850,6225,1664],{"class":1660},[850,6227,6182],{"class":1667},[850,6229,1766],{"class":856},[850,6231,6187],{"class":1676},[850,6233,1680],{"class":1660},[850,6235,1683],{"class":862},[850,6237,797],{"class":856},[850,6239,6240],{"class":1676},"userId",[850,6242,1680],{"class":1660},[850,6244,1683],{"class":862},[850,6246,2745],{"class":856},[850,6248,6249,6251,6254,6256,6258,6260,6263],{"class":852,"line":268},[850,6250,1737],{"class":1660},[850,6252,6253],{"class":862}," doc",[850,6255,1743],{"class":1660},[850,6257,1746],{"class":1660},[850,6259,6200],{"class":856},[850,6261,6262],{"class":1667},"findFirst",[850,6264,2780],{"class":856},[850,6266,6267],{"class":852,"line":1338},[850,6268,6269],{"class":856}," where: { id, userId },\n",[850,6271,6272],{"class":852,"line":1495},[850,6273,6039],{"class":856},[850,6275,6276,6278,6280,6282,6285,6288,6290,6293],{"class":852,"line":1503},[850,6277,2947],{"class":1660},[850,6279,1123],{"class":856},[850,6281,4068],{"class":1660},[850,6283,6284],{"class":856},"doc) ",[850,6286,6287],{"class":1660},"throw",[850,6289,3131],{"class":1660},[850,6291,6292],{"class":1667}," NotFoundError",[850,6294,6113],{"class":856},[850,6296,6297,6299],{"class":852,"line":1514},[850,6298,1757],{"class":1660},[850,6300,6301],{"class":856}," doc;\n",[850,6303,6304],{"class":852,"line":1522},[850,6305,929],{"class":856},[20,6307,6308],{},"For role-based access control (RBAC), check permissions for specific operations, not just user roles:",[734,6310,6312],{"className":1646,"code":6311,"language":1648,"meta":239,"style":239},"function requirePermission(permission: Permission) {\n return (req: Request, res: Response, next: NextFunction) => {\n if (!req.user.permissions.includes(permission)) {\n res.status(403).json({ error: \"Insufficient permissions\" });\n return;\n }\n next();\n };\n}\n\nApp.delete(\n \"/api/documents/:id\",\n authenticate,\n requirePermission(\"documents:delete\"),\n deleteDocument\n);\n",[741,6313,6314,6333,6370,6386,6407,6413,6417,6423,6428,6432,6436,6445,6452,6457,6468,6473],{"__ignoreMap":239},[850,6315,6316,6318,6321,6323,6326,6328,6331],{"class":852,"line":853},[850,6317,4345],{"class":1660},[850,6319,6320],{"class":1667}," requirePermission",[850,6322,1766],{"class":856},[850,6324,6325],{"class":1676},"permission",[850,6327,1680],{"class":1660},[850,6329,6330],{"class":1667}," Permission",[850,6332,2745],{"class":856},[850,6334,6335,6337,6339,6342,6344,6346,6348,6351,6353,6355,6357,6359,6361,6364,6366,6368],{"class":852,"line":243},[850,6336,1757],{"class":1660},[850,6338,1123],{"class":856},[850,6340,6341],{"class":1676},"req",[850,6343,1680],{"class":1660},[850,6345,5933],{"class":1667},[850,6347,797],{"class":856},[850,6349,6350],{"class":1676},"res",[850,6352,1680],{"class":1660},[850,6354,5945],{"class":1667},[850,6356,797],{"class":856},[850,6358,2588],{"class":1676},[850,6360,1680],{"class":1660},[850,6362,6363],{"class":1667}," NextFunction",[850,6365,2591],{"class":856},[850,6367,2594],{"class":1660},[850,6369,1849],{"class":856},[850,6371,6372,6374,6376,6378,6381,6383],{"class":852,"line":240},[850,6373,2947],{"class":1660},[850,6375,1123],{"class":856},[850,6377,4068],{"class":1660},[850,6379,6380],{"class":856},"req.user.permissions.",[850,6382,1763],{"class":1667},[850,6384,6385],{"class":856},"(permission)) {\n",[850,6387,6388,6390,6392,6394,6396,6398,6400,6402,6405],{"class":852,"line":884},[850,6389,6019],{"class":856},[850,6391,6022],{"class":1667},[850,6393,1766],{"class":856},[850,6395,1185],{"class":862},[850,6397,6029],{"class":856},[850,6399,846],{"class":1667},[850,6401,5332],{"class":856},[850,6403,6404],{"class":877},"\"Insufficient permissions\"",[850,6406,6039],{"class":856},[850,6408,6409,6411],{"class":852,"line":897},[850,6410,1757],{"class":1660},[850,6412,1896],{"class":856},[850,6414,6415],{"class":852,"line":910},[850,6416,924],{"class":856},[850,6418,6419,6421],{"class":852,"line":921},[850,6420,2621],{"class":1667},[850,6422,6113],{"class":856},[850,6424,6425],{"class":852,"line":268},[850,6426,6427],{"class":856}," };\n",[850,6429,6430],{"class":852,"line":1338},[850,6431,929],{"class":856},[850,6433,6434],{"class":852,"line":1495},[850,6435,2650],{"emptyLinePlaceholder":266},[850,6437,6438,6440,6443],{"class":852,"line":1503},[850,6439,5166],{"class":856},[850,6441,6442],{"class":1667},"delete",[850,6444,1671],{"class":856},[850,6446,6447,6450],{"class":852,"line":1514},[850,6448,6449],{"class":877}," \"/api/documents/:id\"",[850,6451,881],{"class":856},[850,6453,6454],{"class":852,"line":1522},[850,6455,6456],{"class":856}," authenticate,\n",[850,6458,6459,6461,6463,6466],{"class":852,"line":1530},[850,6460,6320],{"class":1667},[850,6462,1766],{"class":856},[850,6464,6465],{"class":877},"\"documents:delete\"",[850,6467,4235],{"class":856},[850,6469,6470],{"class":852,"line":1541},[850,6471,6472],{"class":856}," deleteDocument\n",[850,6474,6475],{"class":852,"line":1548},[850,6476,5999],{"class":856},[15,6478,2248],{"id":2247},[20,6480,6481],{},"Every public API endpoint needs rate limiting. Without it, your API is vulnerable to:",[211,6483,6484,6487,6490,6493],{},[214,6485,6486],{},"Brute-force attacks on authentication endpoints",[214,6488,6489],{},"Credential stuffing (trying leaked username/password combinations)",[214,6491,6492],{},"Scraping your data at machine speed",[214,6494,6495],{},"Denial of service through request volume",[20,6497,6498],{},"Use a sliding window rate limiter per IP (or per user for authenticated endpoints):",[734,6500,6502],{"className":1646,"code":6501,"language":1648,"meta":239,"style":239},"import rateLimit from \"express-rate-limit\";\nimport RedisStore from \"rate-limit-redis\";\n\n// Strict rate limit for authentication endpoints\nconst authLimiter = rateLimit({\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: 10, // 10 attempts per window\n store: new RedisStore({ client: redisClient }),\n message: { error: \"Too many login attempts, please try again later\" },\n standardHeaders: true,\n});\n\n// General API rate limit\nconst apiLimiter = rateLimit({\n windowMs: 60 * 1000, // 1 minute\n max: 100,\n store: new RedisStore({ client: redisClient }),\n});\n\nApp.use(\"/api/auth\", authLimiter);\napp.use(\"/api\", apiLimiter);\n",[741,6503,6504,6518,6532,6536,6541,6554,6573,6585,6598,6608,6617,6622,6626,6631,6644,6659,6667,6677,6681,6685,6699],{"__ignoreMap":239},[850,6505,6506,6508,6511,6513,6516],{"class":852,"line":853},[850,6507,3546],{"class":1660},[850,6509,6510],{"class":856}," rateLimit ",[850,6512,3552],{"class":1660},[850,6514,6515],{"class":877}," \"express-rate-limit\"",[850,6517,1896],{"class":856},[850,6519,6520,6522,6525,6527,6530],{"class":852,"line":243},[850,6521,3546],{"class":1660},[850,6523,6524],{"class":856}," RedisStore ",[850,6526,3552],{"class":1660},[850,6528,6529],{"class":877}," \"rate-limit-redis\"",[850,6531,1896],{"class":856},[850,6533,6534],{"class":852,"line":240},[850,6535,2650],{"emptyLinePlaceholder":266},[850,6537,6538],{"class":852,"line":884},[850,6539,6540],{"class":1427},"// Strict rate limit for authentication endpoints\n",[850,6542,6543,6545,6548,6550,6552],{"class":852,"line":897},[850,6544,3123],{"class":1660},[850,6546,6547],{"class":862}," authLimiter",[850,6549,1743],{"class":1660},[850,6551,3628],{"class":1667},[850,6553,2780],{"class":856},[850,6555,6556,6558,6560,6562,6564,6566,6568,6570],{"class":852,"line":910},[850,6557,4800],{"class":856},[850,6559,4803],{"class":862},[850,6561,4806],{"class":1660},[850,6563,4809],{"class":862},[850,6565,4806],{"class":1660},[850,6567,4232],{"class":862},[850,6569,797],{"class":856},[850,6571,6572],{"class":1427},"// 15 minutes\n",[850,6574,6575,6578,6580,6582],{"class":852,"line":921},[850,6576,6577],{"class":856}," max: ",[850,6579,4863],{"class":862},[850,6581,797],{"class":856},[850,6583,6584],{"class":1427},"// 10 attempts per window\n",[850,6586,6587,6590,6592,6595],{"class":852,"line":268},[850,6588,6589],{"class":856}," store: ",[850,6591,3369],{"class":1660},[850,6593,6594],{"class":1667}," RedisStore",[850,6596,6597],{"class":856},"({ client: redisClient }),\n",[850,6599,6600,6603,6606],{"class":852,"line":1338},[850,6601,6602],{"class":856}," message: { error: ",[850,6604,6605],{"class":877},"\"Too many login attempts, please try again later\"",[850,6607,4720],{"class":856},[850,6609,6610,6613,6615],{"class":852,"line":1495},[850,6611,6612],{"class":856}," standardHeaders: ",[850,6614,1097],{"class":862},[850,6616,881],{"class":856},[850,6618,6619],{"class":852,"line":1503},[850,6620,6621],{"class":856},"});\n",[850,6623,6624],{"class":852,"line":1514},[850,6625,2650],{"emptyLinePlaceholder":266},[850,6627,6628],{"class":852,"line":1522},[850,6629,6630],{"class":1427},"// General API rate limit\n",[850,6632,6633,6635,6638,6640,6642],{"class":852,"line":1530},[850,6634,3123],{"class":1660},[850,6636,6637],{"class":862}," apiLimiter",[850,6639,1743],{"class":1660},[850,6641,3628],{"class":1667},[850,6643,2780],{"class":856},[850,6645,6646,6648,6650,6652,6654,6656],{"class":852,"line":1541},[850,6647,4800],{"class":856},[850,6649,4936],{"class":862},[850,6651,4806],{"class":1660},[850,6653,4232],{"class":862},[850,6655,797],{"class":856},[850,6657,6658],{"class":1427},"// 1 minute\n",[850,6660,6661,6663,6665],{"class":852,"line":1548},[850,6662,6577],{"class":856},[850,6664,3148],{"class":862},[850,6666,881],{"class":856},[850,6668,6669,6671,6673,6675],{"class":852,"line":1555},[850,6670,6589],{"class":856},[850,6672,3369],{"class":1660},[850,6674,6594],{"class":1667},[850,6676,6597],{"class":856},[850,6678,6679],{"class":852,"line":1562},[850,6680,6621],{"class":856},[850,6682,6683],{"class":852,"line":1572},[850,6684,2650],{"emptyLinePlaceholder":266},[850,6686,6687,6689,6691,6693,6696],{"class":852,"line":1580},[850,6688,5166],{"class":856},[850,6690,2574],{"class":1667},[850,6692,1766],{"class":856},[850,6694,6695],{"class":877},"\"/api/auth\"",[850,6697,6698],{"class":856},", authLimiter);\n",[850,6700,6701,6703,6705,6707,6710],{"class":852,"line":1590},[850,6702,2571],{"class":856},[850,6704,2574],{"class":1667},[850,6706,1766],{"class":856},[850,6708,6709],{"class":877},"\"/api\"",[850,6711,6712],{"class":856},", apiLimiter);\n",[20,6714,6715],{},"Using Redis as the store is important — in-memory storage does not work across multiple instances or restart boundaries. Persist rate limit state in Redis so it survives instance restarts and works correctly in horizontally scaled deployments.",[15,6717,6719],{"id":6718},"input-validation","Input Validation",[20,6721,6722],{},"Validate every input, every time. Not just presence — validate type, length, format, and range:",[734,6724,6726],{"className":1646,"code":6725,"language":1648,"meta":239,"style":239},"import { z } from \"zod\";\n\nConst createPostSchema = z.object({\n title: z.string().min(1).max(200),\n content: z.string().min(1).max(50000),\n tags: z.array(z.string().max(50)).max(10).optional(),\n publishedAt: z.string().datetime().optional(),\n});\n\nApp.post(\"/api/posts\", authenticate, async (req, res) => {\n const result = createPostSchema.safeParse(req.body);\n if (!result.success) {\n return res.status(400).json({\n error: \"Validation failed\",\n details: result.error.flatten(),\n });\n }\n\n const post = await createPost(result.data, req.user.id);\n res.status(201).json(post);\n});\n",[741,6727,6728,6742,6746,6761,6790,6816,6854,6872,6876,6880,6910,6927,6938,6957,6967,6977,6981,6985,6989,7005,7023],{"__ignoreMap":239},[850,6729,6730,6732,6735,6737,6740],{"class":852,"line":853},[850,6731,3546],{"class":1660},[850,6733,6734],{"class":856}," { z } ",[850,6736,3552],{"class":1660},[850,6738,6739],{"class":877}," \"zod\"",[850,6741,1896],{"class":856},[850,6743,6744],{"class":852,"line":243},[850,6745,2650],{"emptyLinePlaceholder":266},[850,6747,6748,6751,6753,6756,6759],{"class":852,"line":240},[850,6749,6750],{"class":856},"Const createPostSchema ",[850,6752,3251],{"class":1660},[850,6754,6755],{"class":856}," z.",[850,6757,6758],{"class":1667},"object",[850,6760,2780],{"class":856},[850,6762,6763,6766,6769,6772,6775,6777,6779,6781,6783,6785,6788],{"class":852,"line":884},[850,6764,6765],{"class":856}," title: z.",[850,6767,6768],{"class":1667},"string",[850,6770,6771],{"class":856},"().",[850,6773,6774],{"class":1667},"min",[850,6776,1766],{"class":856},[850,6778,3976],{"class":862},[850,6780,6029],{"class":856},[850,6782,4034],{"class":1667},[850,6784,1766],{"class":856},[850,6786,6787],{"class":862},"200",[850,6789,4235],{"class":856},[850,6791,6792,6795,6797,6799,6801,6803,6805,6807,6809,6811,6814],{"class":852,"line":897},[850,6793,6794],{"class":856}," content: z.",[850,6796,6768],{"class":1667},[850,6798,6771],{"class":856},[850,6800,6774],{"class":1667},[850,6802,1766],{"class":856},[850,6804,3976],{"class":862},[850,6806,6029],{"class":856},[850,6808,4034],{"class":1667},[850,6810,1766],{"class":856},[850,6812,6813],{"class":862},"50000",[850,6815,4235],{"class":856},[850,6817,6818,6821,6824,6827,6829,6831,6833,6835,6838,6841,6843,6845,6847,6849,6852],{"class":852,"line":910},[850,6819,6820],{"class":856}," tags: z.",[850,6822,6823],{"class":1667},"array",[850,6825,6826],{"class":856},"(z.",[850,6828,6768],{"class":1667},[850,6830,6771],{"class":856},[850,6832,4034],{"class":1667},[850,6834,1766],{"class":856},[850,6836,6837],{"class":862},"50",[850,6839,6840],{"class":856},")).",[850,6842,4034],{"class":1667},[850,6844,1766],{"class":856},[850,6846,4863],{"class":862},[850,6848,6029],{"class":856},[850,6850,6851],{"class":1667},"optional",[850,6853,2692],{"class":856},[850,6855,6856,6859,6861,6863,6866,6868,6870],{"class":852,"line":921},[850,6857,6858],{"class":856}," publishedAt: z.",[850,6860,6768],{"class":1667},[850,6862,6771],{"class":856},[850,6864,6865],{"class":1667},"datetime",[850,6867,6771],{"class":856},[850,6869,6851],{"class":1667},[850,6871,2692],{"class":856},[850,6873,6874],{"class":852,"line":268},[850,6875,6621],{"class":856},[850,6877,6878],{"class":852,"line":1338},[850,6879,2650],{"emptyLinePlaceholder":266},[850,6881,6882,6884,6886,6888,6891,6894,6896,6898,6900,6902,6904,6906,6908],{"class":852,"line":1495},[850,6883,5166],{"class":856},[850,6885,5450],{"class":1667},[850,6887,1766],{"class":856},[850,6889,6890],{"class":877},"\"/api/posts\"",[850,6892,6893],{"class":856},", authenticate, ",[850,6895,1661],{"class":1660},[850,6897,1123],{"class":856},[850,6899,6341],{"class":1676},[850,6901,797],{"class":856},[850,6903,6350],{"class":1676},[850,6905,2591],{"class":856},[850,6907,2594],{"class":1660},[850,6909,1849],{"class":856},[850,6911,6912,6914,6916,6918,6921,6924],{"class":852,"line":1503},[850,6913,1737],{"class":1660},[850,6915,3308],{"class":862},[850,6917,1743],{"class":1660},[850,6919,6920],{"class":856}," createPostSchema.",[850,6922,6923],{"class":1667},"safeParse",[850,6925,6926],{"class":856},"(req.body);\n",[850,6928,6929,6931,6933,6935],{"class":852,"line":1514},[850,6930,2947],{"class":1660},[850,6932,1123],{"class":856},[850,6934,4068],{"class":1660},[850,6936,6937],{"class":856},"result.success) {\n",[850,6939,6940,6942,6944,6946,6948,6951,6953,6955],{"class":852,"line":1522},[850,6941,1757],{"class":1660},[850,6943,6019],{"class":856},[850,6945,6022],{"class":1667},[850,6947,1766],{"class":856},[850,6949,6950],{"class":862},"400",[850,6952,6029],{"class":856},[850,6954,846],{"class":1667},[850,6956,2780],{"class":856},[850,6958,6959,6962,6965],{"class":852,"line":1530},[850,6960,6961],{"class":856}," error: ",[850,6963,6964],{"class":877},"\"Validation failed\"",[850,6966,881],{"class":856},[850,6968,6969,6972,6975],{"class":852,"line":1541},[850,6970,6971],{"class":856}," details: result.error.",[850,6973,6974],{"class":1667},"flatten",[850,6976,2692],{"class":856},[850,6978,6979],{"class":852,"line":1548},[850,6980,6039],{"class":856},[850,6982,6983],{"class":852,"line":1555},[850,6984,924],{"class":856},[850,6986,6987],{"class":852,"line":1562},[850,6988,2650],{"emptyLinePlaceholder":266},[850,6990,6991,6993,6995,6997,6999,7002],{"class":852,"line":1572},[850,6992,1737],{"class":1660},[850,6994,1449],{"class":862},[850,6996,1743],{"class":1660},[850,6998,1746],{"class":1660},[850,7000,7001],{"class":1667}," createPost",[850,7003,7004],{"class":856},"(result.data, req.user.id);\n",[850,7006,7007,7009,7011,7013,7016,7018,7020],{"class":852,"line":1580},[850,7008,6019],{"class":856},[850,7010,6022],{"class":1667},[850,7012,1766],{"class":856},[850,7014,7015],{"class":862},"201",[850,7017,6029],{"class":856},[850,7019,846],{"class":1667},[850,7021,7022],{"class":856},"(post);\n",[850,7024,7025],{"class":852,"line":1590},[850,7026,6621],{"class":856},[20,7028,7029],{},"Validate on the server regardless of any client-side validation. Client-side validation is UX, not security. Assume every request to your API bypasses your frontend completely.",[15,7031,7033],{"id":7032},"output-filtering-return-only-what-is-needed","Output Filtering: Return Only What Is Needed",[20,7035,7036],{},"Never return more data than the caller needs. Your user profile endpoint should not return the password hash, the MFA secret, internal user flags, or other internal fields just because they are on the user object.",[20,7038,7039],{},"Define explicit response schemas and transform your data to match them:",[734,7041,7043],{"className":1646,"code":7042,"language":1648,"meta":239,"style":239},"function sanitizeUser(user: User): PublicUser {\n return {\n id: user.id,\n username: user.username,\n displayName: user.displayName,\n avatarUrl: user.avatarUrl,\n createdAt: user.createdAt,\n // Explicitly exclude: passwordHash, mfaSecret, internalFlags, adminNotes\n };\n}\n\nApp.get(\"/api/users/:id\", async (req, res) => {\n const user = await db.user.findById(req.params.id);\n if (!user) return res.status(404).json({ error: \"Not found\" });\n res.json(sanitizeUser(user));\n});\n",[741,7044,7045,7070,7076,7081,7086,7091,7096,7101,7106,7110,7114,7118,7147,7165,7198,7212],{"__ignoreMap":239},[850,7046,7047,7049,7052,7054,7056,7058,7061,7063,7065,7068],{"class":852,"line":853},[850,7048,4345],{"class":1660},[850,7050,7051],{"class":1667}," sanitizeUser",[850,7053,1766],{"class":856},[850,7055,3240],{"class":1676},[850,7057,1680],{"class":1660},[850,7059,7060],{"class":1667}," User",[850,7062,1718],{"class":856},[850,7064,1680],{"class":1660},[850,7066,7067],{"class":1667}," PublicUser",[850,7069,1849],{"class":856},[850,7071,7072,7074],{"class":852,"line":243},[850,7073,1757],{"class":1660},[850,7075,1849],{"class":856},[850,7077,7078],{"class":852,"line":240},[850,7079,7080],{"class":856}," id: user.id,\n",[850,7082,7083],{"class":852,"line":884},[850,7084,7085],{"class":856}," username: user.username,\n",[850,7087,7088],{"class":852,"line":897},[850,7089,7090],{"class":856}," displayName: user.displayName,\n",[850,7092,7093],{"class":852,"line":910},[850,7094,7095],{"class":856}," avatarUrl: user.avatarUrl,\n",[850,7097,7098],{"class":852,"line":921},[850,7099,7100],{"class":856}," createdAt: user.createdAt,\n",[850,7102,7103],{"class":852,"line":268},[850,7104,7105],{"class":1427}," // Explicitly exclude: passwordHash, mfaSecret, internalFlags, adminNotes\n",[850,7107,7108],{"class":852,"line":1338},[850,7109,6427],{"class":856},[850,7111,7112],{"class":852,"line":1495},[850,7113,929],{"class":856},[850,7115,7116],{"class":852,"line":1503},[850,7117,2650],{"emptyLinePlaceholder":266},[850,7119,7120,7122,7124,7126,7129,7131,7133,7135,7137,7139,7141,7143,7145],{"class":852,"line":1514},[850,7121,5166],{"class":856},[850,7123,2939],{"class":1667},[850,7125,1766],{"class":856},[850,7127,7128],{"class":877},"\"/api/users/:id\"",[850,7130,797],{"class":856},[850,7132,1661],{"class":1660},[850,7134,1123],{"class":856},[850,7136,6341],{"class":1676},[850,7138,797],{"class":856},[850,7140,6350],{"class":1676},[850,7142,2591],{"class":856},[850,7144,2594],{"class":1660},[850,7146,1849],{"class":856},[850,7148,7149,7151,7153,7155,7157,7160,7162],{"class":852,"line":1522},[850,7150,1737],{"class":1660},[850,7152,3196],{"class":862},[850,7154,1743],{"class":1660},[850,7156,1746],{"class":1660},[850,7158,7159],{"class":856}," db.user.",[850,7161,6203],{"class":1667},[850,7163,7164],{"class":856},"(req.params.id);\n",[850,7166,7167,7169,7171,7173,7176,7178,7180,7182,7184,7187,7189,7191,7193,7196],{"class":852,"line":1530},[850,7168,2947],{"class":1660},[850,7170,1123],{"class":856},[850,7172,4068],{"class":1660},[850,7174,7175],{"class":856},"user) ",[850,7177,2953],{"class":1660},[850,7179,6019],{"class":856},[850,7181,6022],{"class":1667},[850,7183,1766],{"class":856},[850,7185,7186],{"class":862},"404",[850,7188,6029],{"class":856},[850,7190,846],{"class":1667},[850,7192,5332],{"class":856},[850,7194,7195],{"class":877},"\"Not found\"",[850,7197,6039],{"class":856},[850,7199,7200,7202,7204,7206,7209],{"class":852,"line":1541},[850,7201,6019],{"class":856},[850,7203,846],{"class":1667},[850,7205,1766],{"class":856},[850,7207,7208],{"class":1667},"sanitizeUser",[850,7210,7211],{"class":856},"(user));\n",[850,7213,7214],{"class":852,"line":1548},[850,7215,6621],{"class":856},[20,7217,7218],{},"This pattern also protects against accidentally adding a new field to your database model and automatically including it in API responses before you intended to.",[15,7220,7222],{"id":7221},"cors-configuration","CORS Configuration",[20,7224,7225],{},"CORS (Cross-Origin Resource Sharing) controls which origins can make requests to your API from a browser. The default browser policy blocks cross-origin requests. CORS headers relax this policy.",[20,7227,7228],{},"Configure CORS explicitly and restrictively:",[734,7230,7232],{"className":1646,"code":7231,"language":1648,"meta":239,"style":239},"import cors from \"cors\";\n\nApp.use(cors({\n origin: process.env.ALLOWED_ORIGINS?.split(\",\") ?? [\"https://yourapp.com\"],\n credentials: true,\n methods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n allowedHeaders: [\"Content-Type\", \"Authorization\"],\n maxAge: 86400, // Cache preflight for 24 hours\n}));\n",[741,7233,7234,7248,7252,7265,7295,7304,7334,7349,7362],{"__ignoreMap":239},[850,7235,7236,7238,7241,7243,7246],{"class":852,"line":853},[850,7237,3546],{"class":1660},[850,7239,7240],{"class":856}," cors ",[850,7242,3552],{"class":1660},[850,7244,7245],{"class":877}," \"cors\"",[850,7247,1896],{"class":856},[850,7249,7250],{"class":852,"line":243},[850,7251,2650],{"emptyLinePlaceholder":266},[850,7253,7254,7256,7258,7260,7263],{"class":852,"line":240},[850,7255,5166],{"class":856},[850,7257,2574],{"class":1667},[850,7259,1766],{"class":856},[850,7261,7262],{"class":1667},"cors",[850,7264,2780],{"class":856},[850,7266,7267,7270,7273,7276,7279,7281,7284,7286,7288,7290,7293],{"class":852,"line":884},[850,7268,7269],{"class":856}," origin: process.env.",[850,7271,7272],{"class":862},"ALLOWED_ORIGINS",[850,7274,7275],{"class":856},"?.",[850,7277,7278],{"class":1667},"split",[850,7280,1766],{"class":856},[850,7282,7283],{"class":877},"\",\"",[850,7285,2591],{"class":856},[850,7287,3994],{"class":1660},[850,7289,3237],{"class":856},[850,7291,7292],{"class":877},"\"https://yourapp.com\"",[850,7294,1068],{"class":856},[850,7296,7297,7300,7302],{"class":852,"line":897},[850,7298,7299],{"class":856}," credentials: ",[850,7301,1097],{"class":862},[850,7303,881],{"class":856},[850,7305,7306,7309,7312,7314,7317,7319,7322,7324,7327,7329,7332],{"class":852,"line":910},[850,7307,7308],{"class":856}," methods: [",[850,7310,7311],{"class":877},"\"GET\"",[850,7313,797],{"class":856},[850,7315,7316],{"class":877},"\"POST\"",[850,7318,797],{"class":856},[850,7320,7321],{"class":877},"\"PUT\"",[850,7323,797],{"class":856},[850,7325,7326],{"class":877},"\"PATCH\"",[850,7328,797],{"class":856},[850,7330,7331],{"class":877},"\"DELETE\"",[850,7333,1068],{"class":856},[850,7335,7336,7339,7342,7344,7347],{"class":852,"line":921},[850,7337,7338],{"class":856}," allowedHeaders: [",[850,7340,7341],{"class":877},"\"Content-Type\"",[850,7343,797],{"class":856},[850,7345,7346],{"class":877},"\"Authorization\"",[850,7348,1068],{"class":856},[850,7350,7351,7354,7357,7359],{"class":852,"line":268},[850,7352,7353],{"class":856}," maxAge: ",[850,7355,7356],{"class":862},"86400",[850,7358,797],{"class":856},[850,7360,7361],{"class":1427},"// Cache preflight for 24 hours\n",[850,7363,7364],{"class":852,"line":1338},[850,7365,7366],{"class":856},"}));\n",[20,7368,7369,7370,7373,7374,7377],{},"Never use ",[741,7371,7372],{},"origin: \"*\""," for APIs that use cookie-based authentication or serve sensitive data. ",[741,7375,7376],{},"*"," prevents cookies from being sent with cross-origin requests (the browser blocks it), so it does not work with credentials anyway — but the intent of a wildcard CORS policy is a signal that authorization enforcement may be lax.",[15,7379,7381],{"id":7380},"api-keys-for-third-party-access","API Keys for Third-Party Access",[20,7383,7384],{},"If you issue API keys for third-party access, treat them as credentials:",[211,7386,7387,7390,7393,7396,7399,7402],{},[214,7388,7389],{},"Generate keys with at least 128 bits of entropy (random, not guessable)",[214,7391,7392],{},"Hash keys before storing (store the hash, return the plain key once at creation)",[214,7394,7395],{},"Associate keys with specific scopes and permissions",[214,7397,7398],{},"Log all API key usage",[214,7400,7401],{},"Allow key rotation and revocation",[214,7403,7404],{},"Set key expiry by default",[734,7406,7408],{"className":1646,"code":7407,"language":1648,"meta":239,"style":239},"import { randomBytes, createHash } from \"crypto\";\n\nFunction generateApiKey(): { key: string; hash: string } {\n const key = `ak_${randomBytes(32).toString(\"hex\")}`;\n const hash = createHash(\"sha256\").update(key).digest(\"hex\");\n return { key, hash };\n}\n",[741,7409,7410,7424,7428,7439,7473,7507,7514],{"__ignoreMap":239},[850,7411,7412,7414,7417,7419,7422],{"class":852,"line":853},[850,7413,3546],{"class":1660},[850,7415,7416],{"class":856}," { randomBytes, createHash } ",[850,7418,3552],{"class":1660},[850,7420,7421],{"class":877}," \"crypto\"",[850,7423,1896],{"class":856},[850,7425,7426],{"class":852,"line":243},[850,7427,2650],{"emptyLinePlaceholder":266},[850,7429,7430,7433,7436],{"class":852,"line":240},[850,7431,7432],{"class":856},"Function ",[850,7434,7435],{"class":1667},"generateApiKey",[850,7437,7438],{"class":856},"(): { key: string; hash: string } {\n",[850,7440,7441,7443,7445,7447,7450,7453,7455,7458,7460,7462,7464,7467,7469,7471],{"class":852,"line":884},[850,7442,1737],{"class":1660},[850,7444,3780],{"class":862},[850,7446,1743],{"class":1660},[850,7448,7449],{"class":877}," `ak_${",[850,7451,7452],{"class":1667},"randomBytes",[850,7454,1766],{"class":877},[850,7456,7457],{"class":862},"32",[850,7459,6029],{"class":877},[850,7461,2689],{"class":1667},[850,7463,1766],{"class":877},[850,7465,7466],{"class":877},"\"hex\"",[850,7468,1718],{"class":877},[850,7470,3912],{"class":877},[850,7472,1896],{"class":856},[850,7474,7475,7477,7480,7482,7485,7487,7490,7492,7495,7498,7501,7503,7505],{"class":852,"line":897},[850,7476,1737],{"class":1660},[850,7478,7479],{"class":862}," hash",[850,7481,1743],{"class":1660},[850,7483,7484],{"class":1667}," createHash",[850,7486,1766],{"class":856},[850,7488,7489],{"class":877},"\"sha256\"",[850,7491,6029],{"class":856},[850,7493,7494],{"class":1667},"update",[850,7496,7497],{"class":856},"(key).",[850,7499,7500],{"class":1667},"digest",[850,7502,1766],{"class":856},[850,7504,7466],{"class":877},[850,7506,5999],{"class":856},[850,7508,7509,7511],{"class":852,"line":910},[850,7510,1757],{"class":1660},[850,7512,7513],{"class":856}," { key, hash };\n",[850,7515,7516],{"class":852,"line":921},[850,7517,929],{"class":856},[20,7519,7520],{},"Display the key to the user once at creation. Store only the hash. If the user loses the key, they generate a new one — you cannot retrieve it.",[15,7522,7524],{"id":7523},"request-logging-for-security-audit","Request Logging for Security Audit",[20,7526,7527],{},"Log enough information to reconstruct what happened when you investigate a security incident:",[734,7529,7531],{"className":1646,"code":7530,"language":1648,"meta":239,"style":239},"app.use((req, res, next) => {\n const start = Date.now();\n res.on(\"finish\", () => {\n logger.info({\n method: req.method,\n path: req.path,\n statusCode: res.statusCode,\n userId: req.user?.id,\n ip: req.ip,\n durationMs: Date.now() - start,\n userAgent: req.headers[\"user-agent\"],\n }, \"API request\");\n });\n next();\n});\n",[741,7532,7533,7557,7571,7590,7600,7605,7610,7615,7620,7625,7639,7649,7658,7662,7668],{"__ignoreMap":239},[850,7534,7535,7537,7539,7541,7543,7545,7547,7549,7551,7553,7555],{"class":852,"line":853},[850,7536,2571],{"class":856},[850,7538,2574],{"class":1667},[850,7540,3338],{"class":856},[850,7542,6341],{"class":1676},[850,7544,797],{"class":856},[850,7546,6350],{"class":1676},[850,7548,797],{"class":856},[850,7550,2588],{"class":1676},[850,7552,2591],{"class":856},[850,7554,2594],{"class":1660},[850,7556,1849],{"class":856},[850,7558,7559,7561,7563,7565,7567,7569],{"class":852,"line":243},[850,7560,1737],{"class":1660},[850,7562,2603],{"class":862},[850,7564,1743],{"class":1660},[850,7566,2608],{"class":856},[850,7568,2611],{"class":1667},[850,7570,6113],{"class":856},[850,7572,7573,7575,7578,7580,7583,7586,7588],{"class":852,"line":240},[850,7574,6019],{"class":856},[850,7576,7577],{"class":1667},"on",[850,7579,1766],{"class":856},[850,7581,7582],{"class":877},"\"finish\"",[850,7584,7585],{"class":856},", () ",[850,7587,2594],{"class":1660},[850,7589,1849],{"class":856},[850,7591,7592,7595,7598],{"class":852,"line":884},[850,7593,7594],{"class":856}," logger.",[850,7596,7597],{"class":1667},"info",[850,7599,2780],{"class":856},[850,7601,7602],{"class":852,"line":897},[850,7603,7604],{"class":856}," method: req.method,\n",[850,7606,7607],{"class":852,"line":910},[850,7608,7609],{"class":856}," path: req.path,\n",[850,7611,7612],{"class":852,"line":921},[850,7613,7614],{"class":856}," statusCode: res.statusCode,\n",[850,7616,7617],{"class":852,"line":268},[850,7618,7619],{"class":856}," userId: req.user?.id,\n",[850,7621,7622],{"class":852,"line":1338},[850,7623,7624],{"class":856}," ip: req.ip,\n",[850,7626,7627,7630,7632,7634,7636],{"class":852,"line":1495},[850,7628,7629],{"class":856}," durationMs: Date.",[850,7631,2611],{"class":1667},[850,7633,2639],{"class":856},[850,7635,2642],{"class":1660},[850,7637,7638],{"class":856}," start,\n",[850,7640,7641,7644,7647],{"class":852,"line":1503},[850,7642,7643],{"class":856}," userAgent: req.headers[",[850,7645,7646],{"class":877},"\"user-agent\"",[850,7648,1068],{"class":856},[850,7650,7651,7653,7656],{"class":852,"line":1514},[850,7652,4725],{"class":856},[850,7654,7655],{"class":877},"\"API request\"",[850,7657,5999],{"class":856},[850,7659,7660],{"class":852,"line":1522},[850,7661,6039],{"class":856},[850,7663,7664,7666],{"class":852,"line":1530},[850,7665,2621],{"class":1667},[850,7667,6113],{"class":856},[850,7669,7670],{"class":852,"line":1541},[850,7671,6621],{"class":856},[20,7673,7674],{},"This log record gives you: who made the request (user ID), from where (IP), what they requested (method + path), whether it succeeded (status code), and how long it took. This is the minimum needed for security audit and incident investigation.",[30,7676],{},[20,7678,7679,7680,778],{},"If you want a security review of your API or help implementing the controls described here, book a session at ",[197,7681,199],{"href":199,"rel":7682},[201],[30,7684],{},[15,7686,209],{"id":208},[211,7688,7689,7695,7701,7707],{},[214,7690,7691],{},[197,7692,7694],{"href":7693},"/blog/input-validation-guide","Input Validation: The First Line of Defense Against Every Attack",[214,7696,7697],{},[197,7698,7700],{"href":7699},"/blog/sql-injection-prevention","SQL Injection Prevention: Why It's Still Happening in 2026 and How to Stop It",[214,7702,7703],{},[197,7704,7706],{"href":7705},"/blog/authentication-security-guide","Authentication Security: What to Get Right Before Your First User Logs In",[214,7708,7709],{},[197,7710,7712],{"href":7711},"/blog/content-security-policy-guide","Content Security Policy: Stopping XSS at the Browser Level",[1309,7714,7715],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":239,"searchDepth":240,"depth":240,"links":7717},[7718,7719,7720,7721,7722,7723,7724,7725,7726],{"id":5878,"depth":243,"text":5879},{"id":6156,"depth":243,"text":6157},{"id":2247,"depth":243,"text":2248},{"id":6718,"depth":243,"text":6719},{"id":7032,"depth":243,"text":7033},{"id":7221,"depth":243,"text":7222},{"id":7380,"depth":243,"text":7381},{"id":7523,"depth":243,"text":7524},{"id":208,"depth":243,"text":209},"Practical API security best practices — authentication schemes, rate limiting, input validation, output filtering, and the production security controls every API needs.",[7729,7730],"API security","API security best practices",{},"/blog/api-security-best-practices",{"title":5862,"description":7727},"blog/api-security-best-practices",[7736,5858,3481,7737],"API Security","REST API","MH73fu6SDfkp0Q5MQaAj263NF_AbknzMetP18ZsDlDk",{"id":7740,"title":7741,"author":7742,"body":7744,"category":7973,"date":257,"description":7974,"extension":259,"featured":260,"image":261,"keywords":7975,"meta":7983,"navigation":266,"path":7984,"readTime":268,"seo":7985,"stem":7986,"tags":7987,"__hash__":7991},"blog/blog/applecross-obeolans-monks-dynasty.md","The O'Beolans of Applecross: The Monks Who Founded a Dynasty",{"name":9,"bio":7743},"Author of The Forge of Tongues — 22,000 Years of Migration, Mutation, and Memory",{"type":12,"value":7745,"toc":7964},[7746,7750,7756,7759,7762,7769,7771,7775,7787,7790,7801,7808,7810,7814,7817,7827,7830,7856,7859,7861,7865,7872,7875,7882,7885,7887,7891,7898,7901,7904,7907,7909,7913,7920,7923,7926,7929,7931,7935,7955,7958],[15,7747,7749],{"id":7748},"the-remote-sanctuary","The Remote Sanctuary",[20,7751,7752,7755],{},[123,7753,7754],{},"A' Chomraich"," — \"The Sanctuary\" in Scottish Gaelic — is the Gaelic name for the Applecross Peninsula on Scotland's northwest coast, facing the islands of Raasay and Skye across the Inner Sound.",[20,7757,7758],{},"The name says everything about what this place once was. In early medieval Scotland, monasteries were not simply places of prayer — they were sanctuaries: areas where violence was forbidden, where the pursued could find refuge, where the law of the church superseded secular authority. The monastery of Applecross, founded in 673 AD, was one such place.",[20,7760,7761],{},"Its reputation for sanctuary extended for six miles in every direction from the monastery site. Anyone who reached that boundary was safe. The monks were the guarantors of the peace.",[20,7763,7764,7765,7768],{},"And for centuries, those monks were the ",[123,7766,7767],{},"O'Beolans"," — a family whose hereditary abbacy at Applecross placed them at the intersection of ecclesiastical authority, traditional clan genealogy, and the political history of northern Scotland. From this monastery on the edge of the Atlantic world came the founding family of Clan Ross.",[30,7770],{},[15,7772,7774],{"id":7773},"maelrubha-and-the-foundation","Maelrubha and the Foundation",[20,7776,7777,7778,7781,7782,7786],{},"The monastery at Applecross was founded by ",[123,7779,7780],{},"Maelrubha"," — in Gaelic, ",[7783,7784,7785],"em",{},"Mael Rubha",", \"the Red Tonsured One\" — an Irish monk who had trained at the monastery of Bangor in County Down, one of the most learned monastic centres in early medieval Ireland.",[20,7788,7789],{},"Maelrubha crossed to Scotland and established his monastery at Applecross in 673 AD. He spent the next fifty years evangelising the surrounding territory, establishing further foundations, and building the institutional presence of Christianity in the northern Highlands. He died in 722 AD at the age of 80, reportedly while on a mission among the Picts near Beauly in the Great Glen.",[20,7791,7792,7793,7796,7797,7800],{},"His cult was significant. ",[123,7794,7795],{},"St. Maelrubha's Day"," (August 27) was celebrated in the northern Highlands for centuries. A local spring at Loch Maree in Wester Ross — ",[7783,7798,7799],{},"Loch ma Ruibhe",", \"Maelrubha's Loch\" — retained religious associations into modern times. Several churches in the region bear dedications to him.",[20,7802,7803,7804,7807],{},"The island of ",[123,7805,7806],{},"Iona"," — Columba's monastery, founded in 563 AD — was the dominant centre of Scottish Christianity in the period, but Applecross represented an independent Irish monastic foundation with its own distinct institutional identity, operating in the territory that would become Clan Ross country.",[30,7809],{},[15,7811,7813],{"id":7812},"the-hereditary-abbacy","The Hereditary Abbacy",[20,7815,7816],{},"After Maelrubha's death, the abbacy of Applecross became hereditary — passing from father to son within a specific family. This was not unusual in the Columban/Irish monastic tradition of the period, which allowed clerical marriage and treated major abbacies as quasi-aristocratic positions held within specific kindreds.",[20,7818,7819,7820,7822,7823,7826],{},"The family that held the Applecross abbacy came to be known as the ",[123,7821,7767],{}," — a name that appears in the genealogical sources connecting them to the broader Dal Riata and Cenél Loairn tradition. The \"O'Beolan\" form is an Irished genealogical framing; in Gaelic terms they were the family of the abbots, the hereditary ",[7783,7824,7825],{},"comarbai"," (successors) of Maelrubha.",[20,7828,7829],{},"The hereditary abbot was not simply a religious figure. In the pre-feudal Highland world, the abbot of a major monastery controlled:",[211,7831,7832,7838,7844,7850],{},[214,7833,7834,7837],{},[123,7835,7836],{},"Land"," — monastic estates extending across the peninsula and beyond",[214,7839,7840,7843],{},[123,7841,7842],{},"Legal jurisdiction"," — the sanctuary rights and the adjudication of disputes within the sanctuary zone",[214,7845,7846,7849],{},[123,7847,7848],{},"Institutional memory"," — the genealogies, the legal traditions, the chronicles that preserved the community's connection to its past",[214,7851,7852,7855],{},[123,7853,7854],{},"Social authority"," — the abbot was a figure to whom secular lords paid respect and sought legitimation",[20,7857,7858],{},"For several centuries — from roughly the eighth to the thirteenth century — the O'Beolans exercised this kind of authority across the Applecross Peninsula and the surrounding territory of Ross-shire. They were, in a real sense, the institutional continuity of the northern Highland Cenél Loairn tradition in the post-Dal Riata period, when secular power structures had fragmented and the church provided the most durable framework for social organisation.",[30,7860],{},[15,7862,7864],{"id":7863},"the-obeolans-and-the-cenél-loairn","The O'Beolans and the Cenél Loairn",[20,7866,7867,7868,7871],{},"The traditional genealogy connects the O'Beolans to the ",[123,7869,7870],{},"Cenél Loairn"," — the kindred of Loarn mac Eirc, the elder brother of Fergus Mór, who had established the northern division of the Scottish Dal Riata around 500 AD.",[20,7873,7874],{},"The strength of this connection varies by how one evaluates the genealogical sources. The genealogical tracts that connect the O'Beolans to the Cenél Loairn were compiled centuries after the events they describe, and medieval genealogists had professional incentives to produce prestigious lineages for their patrons. The probability that the O'Beolan abbots were genuinely descended in a direct biological line from Loarn mac Eirc is perhaps 20–30% — plausible, consistent with the geographic pattern, but not provable from the surviving evidence.",[20,7876,7877,7878,7881],{},"What is more certain is that the O'Beolans ",[7783,7879,7880],{},"occupied the institutional role"," that had been the Cenél Loairn's in the northern territory — they held the abbacy at Applecross, which had been founded in the territory Loarn's kindred had settled, and they maintained the tradition that connected the northern Highland community to its Dal Riata origins.",[20,7883,7884],{},"Whether or not the blood connection to Loarn was direct, the institutional connection was real.",[30,7886],{},[15,7888,7890],{"id":7889},"the-end-of-the-abbacy-fearchar","The End of the Abbacy: Fearchar",[20,7892,7893,7894,7897],{},"The hereditary abbacy of Applecross ended — or rather, transformed — in the early thirteenth century with ",[123,7895,7896],{},"Fearchar mac an t-Sagairt",": Son of the Priest.",[20,7899,7900],{},"The title is telling. \"Priest\" here likely refers to the hereditary abbot — Fearchar's father held the abbacy, making Fearchar the son of the priest-abbot in a family tradition of hereditary religious office. Fearchar himself appears in the documentary record not as an abbot but as a warrior, acting in the service of Alexander II during a rebellion in the northern territories around 1215.",[20,7902,7903],{},"His transition from ecclesiastical lineage to secular earldom marked the broader transformation of Highland society that was underway in the thirteenth century: the feudal reorganisation of Scottish political authority, the replacement of traditional clan and monastic power structures with formal feudal titles that the Scottish crown could grant and revoke, the integration of the Gaelic Highland world into the framework of European feudal governance.",[20,7905,7906],{},"Fearchar navigated this transition successfully. He translated the O'Beolans' traditional authority in Ross — built over centuries through the abbacy — into a feudal earldom recognised by the Scottish crown. The monks became earls. The sanctuary became a county.",[30,7908],{},[15,7910,7912],{"id":7911},"the-monastery-today","The Monastery Today",[20,7914,7915,7916,7919],{},"The site of Maelrubha's original monastery is in the village of ",[123,7917,7918],{},"Applecross"," — the only substantial settlement on the peninsula, reached by the mountain road over the Bealach na Bà or by the coastal road from the north. The original monastic buildings have not survived; the area around the village church is believed to occupy approximately the site of the early medieval monastery.",[20,7921,7922],{},"The village itself is one of the most remote on the Scottish mainland — accessible by two single-track roads, with the inner Sound connecting it by ferry to Raasay. It maintains the feel of a community that has long existed at the edge of things, connected to the sea as much as to the mainland.",[20,7924,7925],{},"Applecross Bay, facing Raasay and Skye with the Cuillin mountains visible on clear days, is one of the most beautiful views in the Highlands. It is not difficult to understand why Maelrubha chose it for his sanctuary — remote enough for contemplation, but connected by sea to the wider Gaelic world.",[20,7927,7928],{},"The monks are long gone. The sanctuary is a memory. But the stone that marks Maelrubha's grave — a weathered cross-slab in the church enclosure — is still there, 1,300 years after the man who founded the institution that would eventually produce the earls of Ross.",[30,7930],{},[15,7932,7934],{"id":7933},"related-articles","Related Articles",[211,7936,7937,7943,7949],{},[214,7938,7939],{},[197,7940,7942],{"href":7941},"/blog/loarn-mac-eirc-elder-brother","Loarn mac Eirc: The Elder Brother and the Senior Blood",[214,7944,7945],{},[197,7946,7948],{"href":7947},"/blog/fearchar-mac-an-t-sagairt-earl-ross","Fearchar mac an t-Sagairt: The First Earl of Ross",[214,7950,7951],{},[197,7952,7954],{"href":7953},"/blog/dal-riata-irish-kingdom-created-scotland","Dal Riata: The Irish Kingdom That Created Scotland",[20,7956,7957],{},"Senior Blood. From a monastery on the edge of the Atlantic world.",[20,7959,7960],{},[197,7961,7963],{"href":7962},"/book","Read the full story of Applecross, the O'Beolans, and Fearchar mac an t-Sagairt in The Forge of Tongues: 22,000 Years of Migration, Mutation, and Memory.",{"title":239,"searchDepth":240,"depth":240,"links":7965},[7966,7967,7968,7969,7970,7971,7972],{"id":7748,"depth":243,"text":7749},{"id":7773,"depth":243,"text":7774},{"id":7812,"depth":243,"text":7813},{"id":7863,"depth":243,"text":7864},{"id":7889,"depth":243,"text":7890},{"id":7911,"depth":243,"text":7912},{"id":7933,"depth":243,"text":7934},"Heritage","For centuries, a hereditary abbatial family called the O'Beolans held the monastery of Applecross on Scotland's remote western coast. When one of their line became the first Earl of Ross, they transformed from ecclesiastical custodians into the founders of one of Scotland's oldest clans.",[7976,7977,7978,7979,7980,7981,7982],"applecross monastery scotland","o'beolans clan ross","maelrubha applecross","hereditary abbacy scotland","clan ross o'beolan","applecross history","scottish highland monastery",{},"/blog/applecross-obeolans-monks-dynasty",{"title":7741,"description":7974},"blog/applecross-obeolans-monks-dynasty",[7918,7767,7988,7989,7780,7990],"Clan Ross History","Scottish Monasticism","Highland History","-wzUNS7nSizsSFD9hXfP0JEW0a4NBqpzrvOFkrrVcQI",{"id":7993,"title":7994,"author":7995,"body":7996,"category":1328,"date":257,"description":8575,"extension":259,"featured":260,"image":261,"keywords":8576,"meta":8581,"navigation":266,"path":8582,"readTime":268,"seo":8583,"stem":8584,"tags":8585,"__hash__":8589},"blog/blog/architecture-decision-records.md","Architecture Decision Records: Why You Need Them and How to Write Them",{"name":9,"bio":10},{"type":12,"value":7997,"toc":8561},[7998,8002,8005,8008,8011,8014,8016,8020,8023,8026,8040,8043,8045,8049,8052,8155,8158,8160,8164,8167,8184,8187,8210,8213,8215,8219,8222,8226,8237,8251,8254,8258,8272,8276,8279,8282,8308,8310,8314,8317,8501,8503,8507,8510,8513,8516,8519,8521,8528,8530,8532,8558],[15,7999,8001],{"id":8000},"the-problem-that-adrs-solve","The Problem That ADRs Solve",[20,8003,8004],{},"Here's a scenario that plays out in almost every engineering organization:",[20,8006,8007],{},"Six months after a major architectural decision, a new engineer joins the team. They look at the system and ask why it's built the way it is. Nobody who made the original decision is available — they've changed teams, left the company, or simply don't remember the details of a discussion that happened on a Tuesday afternoon. The codebase reflects the decision, but not the reasoning behind it.",[20,8009,8010],{},"The new engineer, reasonably, looks at the structure and thinks \"this doesn't make sense.\" They propose a change that seems obviously better given the current context. What they don't know — what nobody told them — is that the \"obvious\" approach was considered and rejected for a specific reason that still applies. The team relitigates a solved problem, sometimes making the same mistake it avoided six months ago.",[20,8012,8013],{},"Architecture Decision Records (ADRs) are the antidote. They capture not just what was decided, but why — the context, the alternatives considered, and the trade-offs accepted. They make architectural decisions a persistent artifact of the project rather than institutional memory that lives only in the heads of the people who were in the room.",[30,8015],{},[15,8017,8019],{"id":8018},"what-an-adr-is-and-isnt","What an ADR Is (and Isn't)",[20,8021,8022],{},"An ADR is a short document that captures a significant architectural decision: what was decided, the context that made the decision necessary, the alternatives that were considered, and the consequences — both the benefits and the costs.",[20,8024,8025],{},"It is not:",[211,8027,8028,8031,8034,8037],{},[214,8029,8030],{},"A design document for the feature itself",[214,8032,8033],{},"A post-mortem or incident review",[214,8035,8036],{},"A proposal for future work",[214,8038,8039],{},"A technical specification",[20,8041,8042],{},"ADRs are narrow by design. Each one covers exactly one decision. They're meant to be written quickly and read quickly. A three-page ADR that takes an hour to write and thirty minutes to read doesn't serve its purpose — the friction is too high and the decision log won't be maintained.",[30,8044],{},[15,8046,8048],{"id":8047},"the-format-that-actually-gets-used","The Format That Actually Gets Used",[20,8050,8051],{},"There are several ADR templates in circulation. After experimenting with most of them, the format I've settled on is a stripped-down version of Michael Nygard's original format:",[734,8053,8057],{"className":8054,"code":8055,"language":8056,"meta":239,"style":239},"language-markdown shiki shiki-themes github-dark","# ADR-NNNN: Title\n\n**Status:** Proposed | Accepted | Deprecated | Superseded by ADR-XXXX\n**Date:** YYYY-MM-DD\n**Deciders:** Names or roles of people who made this decision\n\n## Context\n\nWhat is the situation that makes this decision necessary? What forces are at play — technical constraints, business requirements, team capabilities, organizational structure? What would happen if you didn't make this decision?\n\n## Decision\n\nWhat was decided? State it clearly and without hedging. \"We will use X\" not \"We might consider X.\"\n\n## Consequences\n\nWhat becomes easier because of this decision? What becomes harder? What new problems does it create? What technical debt is being accepted?\n\n## Alternatives Considered\n\nList the alternatives you seriously evaluated. For each, note why it was not chosen. This is often the most valuable part.\n","markdown",[741,8058,8059,8064,8068,8073,8078,8083,8087,8092,8096,8101,8105,8110,8114,8119,8123,8128,8132,8137,8141,8146,8150],{"__ignoreMap":239},[850,8060,8061],{"class":852,"line":853},[850,8062,8063],{},"# ADR-NNNN: Title\n",[850,8065,8066],{"class":852,"line":243},[850,8067,2650],{"emptyLinePlaceholder":266},[850,8069,8070],{"class":852,"line":240},[850,8071,8072],{},"**Status:** Proposed | Accepted | Deprecated | Superseded by ADR-XXXX\n",[850,8074,8075],{"class":852,"line":884},[850,8076,8077],{},"**Date:** YYYY-MM-DD\n",[850,8079,8080],{"class":852,"line":897},[850,8081,8082],{},"**Deciders:** Names or roles of people who made this decision\n",[850,8084,8085],{"class":852,"line":910},[850,8086,2650],{"emptyLinePlaceholder":266},[850,8088,8089],{"class":852,"line":921},[850,8090,8091],{},"## Context\n",[850,8093,8094],{"class":852,"line":268},[850,8095,2650],{"emptyLinePlaceholder":266},[850,8097,8098],{"class":852,"line":1338},[850,8099,8100],{},"What is the situation that makes this decision necessary? What forces are at play — technical constraints, business requirements, team capabilities, organizational structure? What would happen if you didn't make this decision?\n",[850,8102,8103],{"class":852,"line":1495},[850,8104,2650],{"emptyLinePlaceholder":266},[850,8106,8107],{"class":852,"line":1503},[850,8108,8109],{},"## Decision\n",[850,8111,8112],{"class":852,"line":1514},[850,8113,2650],{"emptyLinePlaceholder":266},[850,8115,8116],{"class":852,"line":1522},[850,8117,8118],{},"What was decided? State it clearly and without hedging. \"We will use X\" not \"We might consider X.\"\n",[850,8120,8121],{"class":852,"line":1530},[850,8122,2650],{"emptyLinePlaceholder":266},[850,8124,8125],{"class":852,"line":1541},[850,8126,8127],{},"## Consequences\n",[850,8129,8130],{"class":852,"line":1548},[850,8131,2650],{"emptyLinePlaceholder":266},[850,8133,8134],{"class":852,"line":1555},[850,8135,8136],{},"What becomes easier because of this decision? What becomes harder? What new problems does it create? What technical debt is being accepted?\n",[850,8138,8139],{"class":852,"line":1562},[850,8140,2650],{"emptyLinePlaceholder":266},[850,8142,8143],{"class":852,"line":1572},[850,8144,8145],{},"## Alternatives Considered\n",[850,8147,8148],{"class":852,"line":1580},[850,8149,2650],{"emptyLinePlaceholder":266},[850,8151,8152],{"class":852,"line":1590},[850,8153,8154],{},"List the alternatives you seriously evaluated. For each, note why it was not chosen. This is often the most valuable part.\n",[20,8156,8157],{},"The section that gets skipped most often is \"Alternatives Considered,\" and it's the most important one. The alternatives section is what explains the decision to someone who doesn't have your context. \"We chose Kafka over RabbitMQ\" is less useful than \"We chose Kafka over RabbitMQ because we need log replay capability for our audit requirements and the ability to support multiple independent consumers without message deletion — RabbitMQ's queue model would have required significant workarounds for both.\"",[30,8159],{},[15,8161,8163],{"id":8162},"when-to-write-an-adr","When to Write an ADR",[20,8165,8166],{},"Not every decision needs an ADR. If you did, you'd spend all your time writing documentation instead of building software. The guideline I use: write an ADR for any decision that:",[211,8168,8169,8172,8175,8178,8181],{},[214,8170,8171],{},"Is expensive to reverse once made",[214,8173,8174],{},"Affects multiple teams or services",[214,8176,8177],{},"Involves non-obvious trade-offs that reasonable engineers might evaluate differently",[214,8179,8180],{},"Would benefit from being explicitly documented for future team members",[214,8182,8183],{},"Involves rejecting a seemingly obvious approach for non-obvious reasons",[20,8185,8186],{},"Practical triggers:",[211,8188,8189,8192,8195,8198,8201,8204,8207],{},[214,8190,8191],{},"Choosing a database for a new service",[214,8193,8194],{},"Deciding between synchronous and asynchronous communication for a critical flow",[214,8196,8197],{},"Adopting a new framework, language, or platform",[214,8199,8200],{},"Defining an API contract that other teams will consume",[214,8202,8203],{},"Choosing a service decomposition strategy",[214,8205,8206],{},"Deciding how to handle authentication and authorization across services",[214,8208,8209],{},"Any significant security architecture decision",[20,8211,8212],{},"You do not need an ADR for: naming conventions (put those in a style guide), local implementation choices that don't affect interfaces, or decisions that are trivially reversible.",[30,8214],{},[15,8216,8218],{"id":8217},"building-a-decision-log-that-survives","Building a Decision Log That Survives",[20,8220,8221],{},"An ADR written in isolation is useful. An ADR that's part of a maintained decision log is invaluable. The difference is findability and completeness.",[52,8223,8225],{"id":8224},"where-to-store-adrs","Where to Store ADRs",[20,8227,8228,8229,8232,8233,8236],{},"The best place for ADRs is close to the code — typically a ",[741,8230,8231],{},"docs/decisions/"," or ",[741,8234,8235],{},"decisions/"," directory in the repository. This has several advantages:",[211,8238,8239,8242,8245,8248],{},[214,8240,8241],{},"ADRs are versioned alongside the code they describe",[214,8243,8244],{},"They appear in code searches",[214,8246,8247],{},"Pull requests for code changes can link to or create corresponding ADRs",[214,8249,8250],{},"Engineers encounter them naturally when exploring the codebase",[20,8252,8253],{},"For multi-repository organizations, a central architecture repository or wiki can supplement per-repo ADRs, but the per-repo location should be the primary home for decisions about that service.",[52,8255,8257],{"id":8256},"numbering-and-naming","Numbering and Naming",[20,8259,8260,8261,797,8264,8267,8268,8271],{},"Sequential numbering makes ADRs easy to reference: ",[741,8262,8263],{},"ADR-0001",[741,8265,8266],{},"ADR-0002",". File names follow the pattern: ",[741,8269,8270],{},"ADR-0001-choose-postgresql-for-user-data.md",". The number provides ordering; the name provides instant context.",[52,8273,8275],{"id":8274},"keeping-adrs-current","Keeping ADRs Current",[20,8277,8278],{},"ADRs should be immutable once accepted. When a decision is superseded, you don't edit the original — you write a new ADR that explicitly supersedes it. The original remains as a record that the old decision existed and why it's no longer in effect.",[20,8280,8281],{},"Update the status field:",[211,8283,8284,8290,8296,8302],{},[214,8285,8286,8289],{},[741,8287,8288],{},"Proposed"," — under discussion",[214,8291,8292,8295],{},[741,8293,8294],{},"Accepted"," — in effect",[214,8297,8298,8301],{},[741,8299,8300],{},"Deprecated"," — no longer recommended but not actively replaced",[214,8303,8304,8307],{},[741,8305,8306],{},"Superseded by ADR-0042"," — replaced by a newer decision",[30,8309],{},[15,8311,8313],{"id":8312},"an-example-adr","An Example ADR",[20,8315,8316],{},"To make this concrete:",[734,8318,8320],{"className":8054,"code":8319,"language":8056,"meta":239,"style":239},"# ADR-0014: Use PostgreSQL for the Orders Service\n\n**Status:** Accepted\n**Date:** 2026-02-15\n**Deciders:** James Ross (Architect), Sarah Chen (Orders Team Lead)\n\n## Context\n\nThe Orders service needs a persistent data store for order records, payment state,\nand order line items. The service processes approximately 500 order writes per minute\nat peak with complex relational queries for order history and reporting.\n\n## Decision\n\nWe will use PostgreSQL as the primary data store for the Orders service.\n\n## Consequences\n\nPositive:\n- Strong ACID guarantees for financial transactions\n- Mature tooling for migrations, backups, and replication\n- Support for complex reporting queries without a separate reporting store\n- Team has existing operational expertise\n\nNegative:\n- Vertical scaling limits will require attention if order volume grows 10x+\n- We accept some schema migration overhead compared to document stores\n\n## Alternatives Considered\n\n**MongoDB:** Rejected because our order model is highly relational and the lack of\ncross-document transactions would complicate financial consistency guarantees.\n\n**MySQL:** Technically viable, but the team has deeper PostgreSQL expertise and\nPostgreSQL's JSON support is superior for order metadata storage.\n\n**DynamoDB:** Rejected because reporting queries across the full order history would\nrequire a secondary data store (e.g., DynamoDB Streams + Redshift), adding operational\ncomplexity without clear throughput benefits at our current scale.\n",[741,8321,8322,8327,8331,8336,8341,8346,8350,8354,8358,8363,8368,8373,8377,8381,8385,8390,8394,8398,8402,8407,8412,8417,8422,8427,8431,8436,8441,8446,8450,8454,8458,8463,8468,8472,8477,8482,8486,8491,8496],{"__ignoreMap":239},[850,8323,8324],{"class":852,"line":853},[850,8325,8326],{},"# ADR-0014: Use PostgreSQL for the Orders Service\n",[850,8328,8329],{"class":852,"line":243},[850,8330,2650],{"emptyLinePlaceholder":266},[850,8332,8333],{"class":852,"line":240},[850,8334,8335],{},"**Status:** Accepted\n",[850,8337,8338],{"class":852,"line":884},[850,8339,8340],{},"**Date:** 2026-02-15\n",[850,8342,8343],{"class":852,"line":897},[850,8344,8345],{},"**Deciders:** James Ross (Architect), Sarah Chen (Orders Team Lead)\n",[850,8347,8348],{"class":852,"line":910},[850,8349,2650],{"emptyLinePlaceholder":266},[850,8351,8352],{"class":852,"line":921},[850,8353,8091],{},[850,8355,8356],{"class":852,"line":268},[850,8357,2650],{"emptyLinePlaceholder":266},[850,8359,8360],{"class":852,"line":1338},[850,8361,8362],{},"The Orders service needs a persistent data store for order records, payment state,\n",[850,8364,8365],{"class":852,"line":1495},[850,8366,8367],{},"and order line items. The service processes approximately 500 order writes per minute\n",[850,8369,8370],{"class":852,"line":1503},[850,8371,8372],{},"at peak with complex relational queries for order history and reporting.\n",[850,8374,8375],{"class":852,"line":1514},[850,8376,2650],{"emptyLinePlaceholder":266},[850,8378,8379],{"class":852,"line":1522},[850,8380,8109],{},[850,8382,8383],{"class":852,"line":1530},[850,8384,2650],{"emptyLinePlaceholder":266},[850,8386,8387],{"class":852,"line":1541},[850,8388,8389],{},"We will use PostgreSQL as the primary data store for the Orders service.\n",[850,8391,8392],{"class":852,"line":1548},[850,8393,2650],{"emptyLinePlaceholder":266},[850,8395,8396],{"class":852,"line":1555},[850,8397,8127],{},[850,8399,8400],{"class":852,"line":1562},[850,8401,2650],{"emptyLinePlaceholder":266},[850,8403,8404],{"class":852,"line":1572},[850,8405,8406],{},"Positive:\n",[850,8408,8409],{"class":852,"line":1580},[850,8410,8411],{},"- Strong ACID guarantees for financial transactions\n",[850,8413,8414],{"class":852,"line":1590},[850,8415,8416],{},"- Mature tooling for migrations, backups, and replication\n",[850,8418,8419],{"class":852,"line":1597},[850,8420,8421],{},"- Support for complex reporting queries without a separate reporting store\n",[850,8423,8424],{"class":852,"line":1604},[850,8425,8426],{},"- Team has existing operational expertise\n",[850,8428,8429],{"class":852,"line":1611},[850,8430,2650],{"emptyLinePlaceholder":266},[850,8432,8433],{"class":852,"line":3798},[850,8434,8435],{},"Negative:\n",[850,8437,8438],{"class":852,"line":3803},[850,8439,8440],{},"- Vertical scaling limits will require attention if order volume grows 10x+\n",[850,8442,8443],{"class":852,"line":3820},[850,8444,8445],{},"- We accept some schema migration overhead compared to document stores\n",[850,8447,8448],{"class":852,"line":3825},[850,8449,2650],{"emptyLinePlaceholder":266},[850,8451,8452],{"class":852,"line":3831},[850,8453,8145],{},[850,8455,8456],{"class":852,"line":3849},[850,8457,2650],{"emptyLinePlaceholder":266},[850,8459,8460],{"class":852,"line":3854},[850,8461,8462],{},"**MongoDB:** Rejected because our order model is highly relational and the lack of\n",[850,8464,8465],{"class":852,"line":3860},[850,8466,8467],{},"cross-document transactions would complicate financial consistency guarantees.\n",[850,8469,8470],{"class":852,"line":3871},[850,8471,2650],{"emptyLinePlaceholder":266},[850,8473,8474],{"class":852,"line":3876},[850,8475,8476],{},"**MySQL:** Technically viable, but the team has deeper PostgreSQL expertise and\n",[850,8478,8479],{"class":852,"line":3882},[850,8480,8481],{},"PostgreSQL's JSON support is superior for order metadata storage.\n",[850,8483,8484],{"class":852,"line":3917},[850,8485,2650],{"emptyLinePlaceholder":266},[850,8487,8488],{"class":852,"line":3922},[850,8489,8490],{},"**DynamoDB:** Rejected because reporting queries across the full order history would\n",[850,8492,8493],{"class":852,"line":3928},[850,8494,8495],{},"require a secondary data store (e.g., DynamoDB Streams + Redshift), adding operational\n",[850,8497,8498],{"class":852,"line":3939},[850,8499,8500],{},"complexity without clear throughput benefits at our current scale.\n",[30,8502],{},[15,8504,8506],{"id":8505},"start-small-and-build-the-habit","Start Small and Build the Habit",[20,8508,8509],{},"The most common failure mode with ADRs is never starting. Teams think they need a perfect system before they begin, or they think they'll backfill decisions from the past six months before writing new ones.",[20,8511,8512],{},"Don't. Start with the next architectural decision you make. Write the ADR as part of the decision-making process, not after. Put it in the repository. Reference it in the PR. Tell the team where to find it.",[20,8514,8515],{},"After ten ADRs, the habit forms. After fifty, the decision log becomes genuinely useful. After a year, new engineers onboard faster because the reasoning behind the system's design is documented and findable.",[20,8517,8518],{},"It takes less time than you think, and the return is compounding.",[30,8520],{},[20,8522,8523,8524],{},"If you're building out an architectural documentation practice or want to talk through your decision-making process, ",[197,8525,8527],{"href":199,"rel":8526},[201],"I'm happy to help.",[30,8529],{},[15,8531,209],{"id":208},[211,8533,8534,8540,8546,8552],{},[214,8535,8536],{},[197,8537,8539],{"href":8538},"/blog/software-documentation-best-practices","Software Documentation That Engineers Actually Read",[214,8541,8542],{},[197,8543,8545],{"href":8544},"/blog/clean-architecture-guide","Clean Architecture in Practice (Beyond the Circles Diagram)",[214,8547,8548],{},[197,8549,8551],{"href":8550},"/blog/event-driven-architecture-guide","Event-Driven Architecture: When It's the Right Call",[214,8553,8554],{},[197,8555,8557],{"href":8556},"/blog/hexagonal-architecture-guide","Hexagonal Architecture: Ports, Adapters, and the Core That Never Changes",[1309,8559,8560],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":239,"searchDepth":240,"depth":240,"links":8562},[8563,8564,8565,8566,8567,8572,8573,8574],{"id":8000,"depth":243,"text":8001},{"id":8018,"depth":243,"text":8019},{"id":8047,"depth":243,"text":8048},{"id":8162,"depth":243,"text":8163},{"id":8217,"depth":243,"text":8218,"children":8568},[8569,8570,8571],{"id":8224,"depth":240,"text":8225},{"id":8256,"depth":240,"text":8257},{"id":8274,"depth":240,"text":8275},{"id":8312,"depth":243,"text":8313},{"id":8505,"depth":243,"text":8506},{"id":208,"depth":243,"text":209},"Architecture Decision Records (ADRs) are the most underused tool in software architecture. Here's why they matter, what format actually works, and how to build a decision log your team will use.",[8577,8578,8579,8580],"architecture decision records ADR","ADR format","architecture documentation","engineering decision log",{},"/blog/architecture-decision-records",{"title":7994,"description":8575},"blog/architecture-decision-records",[8586,1345,8587,8588],"Architecture Decision Records","Documentation","Engineering Culture","En-buBrdFx6zjnLBFHpnUlSuUZCDcd88hJ8bsmIEYm8",{"id":8591,"title":7706,"author":8592,"body":8593,"category":5858,"date":257,"description":10105,"extension":259,"featured":260,"image":261,"keywords":10106,"meta":10109,"navigation":266,"path":7705,"readTime":268,"seo":10110,"stem":10111,"tags":10112,"__hash__":10116},"blog/blog/authentication-security-guide.md",{"name":9,"bio":10},{"type":12,"value":8594,"toc":10095},[8595,8598,8601,8604,8608,8611,8614,8617,8773,8776,8779,8943,8946,8950,8953,8970,8973,9185,9188,9192,9195,9198,9259,9262,9265,9366,9369,9479,9483,9486,9489,9495,9501,9507,9591,9595,9598,9601,9782,9785,9789,9792,9813,9816,9827,10024,10028,10031,10060,10062,10068,10070,10072,10092],[5867,8596,7706],{"id":8597},"authentication-security-what-to-get-right-before-your-first-user-logs-in",[20,8599,8600],{},"Authentication is the foundation your entire security model rests on. Get it right and every user is who they claim to be. Get it wrong and every other security control in your application is undermined — there is no point protecting resources from unauthorized access if unauthorized people can authenticate as authorized users.",[20,8602,8603],{},"I have seen authentication implemented badly in more production applications than I care to count. The mistakes are often the same: fast password hashing, no rate limiting, inadequate session management, poorly implemented reset flows. Here is what correct looks like.",[15,8605,8607],{"id":8606},"password-hashing-the-non-negotiable","Password Hashing: The Non-Negotiable",[20,8609,8610],{},"Passwords must never be stored in plaintext. This is not a controversial statement, yet plaintext password storage shows up in breach reports regularly. When your database is compromised — and treat it as \"when,\" not \"if\" — you want to ensure that your users' passwords cannot be recovered from the leaked data.",[20,8612,8613],{},"Password hashing uses a one-way function to transform a password into a hash. Given the hash, you cannot recover the password. Given the password, you can verify it produces the same hash.",[20,8615,8616],{},"Use bcrypt, Argon2id, or scrypt. These are specifically designed for password hashing — they are intentionally slow, making brute-force attacks expensive.",[734,8618,8620],{"className":1646,"code":8619,"language":1648,"meta":239,"style":239},"import bcrypt from \"bcrypt\";\n\n// Hashing — use 12 rounds minimum\nconst BCRYPT_ROUNDS = 12;\n\nAsync function hashPassword(password: string): Promise\u003Cstring> {\n return bcrypt.hash(password, BCRYPT_ROUNDS);\n}\n\nAsync function verifyPassword(password: string, hash: string): Promise\u003Cboolean> {\n return bcrypt.compare(password, hash);\n}\n",[741,8621,8622,8636,8640,8645,8659,8663,8694,8712,8716,8720,8757,8769],{"__ignoreMap":239},[850,8623,8624,8626,8629,8631,8634],{"class":852,"line":853},[850,8625,3546],{"class":1660},[850,8627,8628],{"class":856}," bcrypt ",[850,8630,3552],{"class":1660},[850,8632,8633],{"class":877}," \"bcrypt\"",[850,8635,1896],{"class":856},[850,8637,8638],{"class":852,"line":243},[850,8639,2650],{"emptyLinePlaceholder":266},[850,8641,8642],{"class":852,"line":240},[850,8643,8644],{"class":1427},"// Hashing — use 12 rounds minimum\n",[850,8646,8647,8649,8652,8654,8657],{"class":852,"line":884},[850,8648,3123],{"class":1660},[850,8650,8651],{"class":862}," BCRYPT_ROUNDS",[850,8653,1743],{"class":1660},[850,8655,8656],{"class":862}," 12",[850,8658,1896],{"class":856},[850,8660,8661],{"class":852,"line":897},[850,8662,2650],{"emptyLinePlaceholder":266},[850,8664,8665,8668,8670,8673,8675,8678,8680,8682,8684,8686,8688,8690,8692],{"class":852,"line":910},[850,8666,8667],{"class":856},"Async ",[850,8669,4345],{"class":1660},[850,8671,8672],{"class":1667}," hashPassword",[850,8674,1766],{"class":856},[850,8676,8677],{"class":1676},"password",[850,8679,1680],{"class":1660},[850,8681,1683],{"class":862},[850,8683,1718],{"class":856},[850,8685,1680],{"class":1660},[850,8687,1723],{"class":1667},[850,8689,1726],{"class":856},[850,8691,6768],{"class":862},[850,8693,1732],{"class":856},[850,8695,8696,8698,8701,8704,8707,8710],{"class":852,"line":921},[850,8697,1757],{"class":1660},[850,8699,8700],{"class":856}," bcrypt.",[850,8702,8703],{"class":1667},"hash",[850,8705,8706],{"class":856},"(password, ",[850,8708,8709],{"class":862},"BCRYPT_ROUNDS",[850,8711,5999],{"class":856},[850,8713,8714],{"class":852,"line":268},[850,8715,929],{"class":856},[850,8717,8718],{"class":852,"line":1338},[850,8719,2650],{"emptyLinePlaceholder":266},[850,8721,8722,8724,8726,8729,8731,8733,8735,8737,8739,8741,8743,8745,8747,8749,8751,8753,8755],{"class":852,"line":1495},[850,8723,8667],{"class":856},[850,8725,4345],{"class":1660},[850,8727,8728],{"class":1667}," verifyPassword",[850,8730,1766],{"class":856},[850,8732,8677],{"class":1676},[850,8734,1680],{"class":1660},[850,8736,1683],{"class":862},[850,8738,797],{"class":856},[850,8740,8703],{"class":1676},[850,8742,1680],{"class":1660},[850,8744,1683],{"class":862},[850,8746,1718],{"class":856},[850,8748,1680],{"class":1660},[850,8750,1723],{"class":1667},[850,8752,1726],{"class":856},[850,8754,1729],{"class":862},[850,8756,1732],{"class":856},[850,8758,8759,8761,8763,8766],{"class":852,"line":1503},[850,8760,1757],{"class":1660},[850,8762,8700],{"class":856},[850,8764,8765],{"class":1667},"compare",[850,8767,8768],{"class":856},"(password, hash);\n",[850,8770,8771],{"class":852,"line":1514},[850,8772,929],{"class":856},[20,8774,8775],{},"Why bcrypt over SHA-256 or MD5? SHA-256 is designed to be fast — a modern GPU can compute billions of SHA-256 hashes per second, making rainbow table and brute-force attacks feasible. Bcrypt with 12 rounds takes approximately 300ms per hash — fast enough for legitimate users to barely notice, slow enough that brute-forcing a database of hashed passwords is computationally infeasible.",[20,8777,8778],{},"Argon2id is the current best practice for new implementations:",[734,8780,8782],{"className":1646,"code":8781,"language":1648,"meta":239,"style":239},"import argon2 from \"argon2\";\n\nAsync function hashPassword(password: string): Promise\u003Cstring> {\n return argon2.hash(password, {\n type: argon2.argon2id,\n memoryCost: 65536, // 64MB\n timeCost: 3,\n parallelism: 4,\n });\n}\n\nAsync function verifyPassword(password: string, hash: string): Promise\u003Cboolean> {\n return argon2.verify(hash, password);\n}\n",[741,8783,8784,8798,8802,8830,8842,8847,8860,8870,8880,8884,8888,8892,8928,8939],{"__ignoreMap":239},[850,8785,8786,8788,8791,8793,8796],{"class":852,"line":853},[850,8787,3546],{"class":1660},[850,8789,8790],{"class":856}," argon2 ",[850,8792,3552],{"class":1660},[850,8794,8795],{"class":877}," \"argon2\"",[850,8797,1896],{"class":856},[850,8799,8800],{"class":852,"line":243},[850,8801,2650],{"emptyLinePlaceholder":266},[850,8803,8804,8806,8808,8810,8812,8814,8816,8818,8820,8822,8824,8826,8828],{"class":852,"line":240},[850,8805,8667],{"class":856},[850,8807,4345],{"class":1660},[850,8809,8672],{"class":1667},[850,8811,1766],{"class":856},[850,8813,8677],{"class":1676},[850,8815,1680],{"class":1660},[850,8817,1683],{"class":862},[850,8819,1718],{"class":856},[850,8821,1680],{"class":1660},[850,8823,1723],{"class":1667},[850,8825,1726],{"class":856},[850,8827,6768],{"class":862},[850,8829,1732],{"class":856},[850,8831,8832,8834,8837,8839],{"class":852,"line":884},[850,8833,1757],{"class":1660},[850,8835,8836],{"class":856}," argon2.",[850,8838,8703],{"class":1667},[850,8840,8841],{"class":856},"(password, {\n",[850,8843,8844],{"class":852,"line":897},[850,8845,8846],{"class":856}," type: argon2.argon2id,\n",[850,8848,8849,8852,8855,8857],{"class":852,"line":910},[850,8850,8851],{"class":856}," memoryCost: ",[850,8853,8854],{"class":862},"65536",[850,8856,797],{"class":856},[850,8858,8859],{"class":1427},"// 64MB\n",[850,8861,8862,8865,8868],{"class":852,"line":921},[850,8863,8864],{"class":856}," timeCost: ",[850,8866,8867],{"class":862},"3",[850,8869,881],{"class":856},[850,8871,8872,8875,8878],{"class":852,"line":268},[850,8873,8874],{"class":856}," parallelism: ",[850,8876,8877],{"class":862},"4",[850,8879,881],{"class":856},[850,8881,8882],{"class":852,"line":1338},[850,8883,6039],{"class":856},[850,8885,8886],{"class":852,"line":1495},[850,8887,929],{"class":856},[850,8889,8890],{"class":852,"line":1503},[850,8891,2650],{"emptyLinePlaceholder":266},[850,8893,8894,8896,8898,8900,8902,8904,8906,8908,8910,8912,8914,8916,8918,8920,8922,8924,8926],{"class":852,"line":1514},[850,8895,8667],{"class":856},[850,8897,4345],{"class":1660},[850,8899,8728],{"class":1667},[850,8901,1766],{"class":856},[850,8903,8677],{"class":1676},[850,8905,1680],{"class":1660},[850,8907,1683],{"class":862},[850,8909,797],{"class":856},[850,8911,8703],{"class":1676},[850,8913,1680],{"class":1660},[850,8915,1683],{"class":862},[850,8917,1718],{"class":856},[850,8919,1680],{"class":1660},[850,8921,1723],{"class":1667},[850,8923,1726],{"class":856},[850,8925,1729],{"class":862},[850,8927,1732],{"class":856},[850,8929,8930,8932,8934,8936],{"class":852,"line":1522},[850,8931,1757],{"class":1660},[850,8933,8836],{"class":856},[850,8935,6074],{"class":1667},[850,8937,8938],{"class":856},"(hash, password);\n",[850,8940,8941],{"class":852,"line":1530},[850,8942,929],{"class":856},[20,8944,8945],{},"Argon2id won the Password Hashing Competition in 2015 and is recommended by OWASP. It requires configuring memory cost (resistant to GPU attacks) and time cost. The parameters above are a reasonable starting point.",[15,8947,8949],{"id":8948},"password-policy-in-2026","Password Policy in 2026",[20,8951,8952],{},"NIST's 2024 guidelines (SP 800-63B) have changed best practices significantly. Current recommendations:",[211,8954,8955,8958,8961,8964,8967],{},[214,8956,8957],{},"Minimum length: 8 characters (15+ recommended)",[214,8959,8960],{},"Maximum length: at least 64 characters (many systems truncate long passwords — this is wrong)",[214,8962,8963],{},"Do not require special characters, numbers, or mixed case (complexity requirements lead to predictable patterns)",[214,8965,8966],{},"Do require checking against known breached passwords",[214,8968,8969],{},"Do not force periodic password changes (unless there is evidence of compromise)",[20,8971,8972],{},"Check passwords against the Have I Been Pwned database. HIBP exposes an API for k-anonymity password checking — you send the first 5 characters of the SHA-1 hash of the password, and receive a list of hashes to check against locally. The full password never leaves your system:",[734,8974,8976],{"className":1646,"code":8975,"language":1648,"meta":239,"style":239},"async function isBreachedPassword(password: string): Promise\u003Cboolean> {\n const hash = crypto.createHash(\"sha1\").update(password).toUpperCase().digest(\"hex\");\n const prefix = hash.slice(0, 5);\n const suffix = hash.slice(5);\n\n const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);\n const text = await response.text();\n\n return text.split(\"\\r\\n\").some((line) => line.startsWith(suffix));\n}\n",[741,8977,8978,9007,9046,9071,9090,9094,9118,9136,9140,9181],{"__ignoreMap":239},[850,8979,8980,8982,8984,8987,8989,8991,8993,8995,8997,8999,9001,9003,9005],{"class":852,"line":853},[850,8981,1661],{"class":1660},[850,8983,1664],{"class":1660},[850,8985,8986],{"class":1667}," isBreachedPassword",[850,8988,1766],{"class":856},[850,8990,8677],{"class":1676},[850,8992,1680],{"class":1660},[850,8994,1683],{"class":862},[850,8996,1718],{"class":856},[850,8998,1680],{"class":1660},[850,9000,1723],{"class":1667},[850,9002,1726],{"class":856},[850,9004,1729],{"class":862},[850,9006,1732],{"class":856},[850,9008,9009,9011,9013,9015,9018,9021,9023,9026,9028,9030,9033,9036,9038,9040,9042,9044],{"class":852,"line":243},[850,9010,1737],{"class":1660},[850,9012,7479],{"class":862},[850,9014,1743],{"class":1660},[850,9016,9017],{"class":856}," crypto.",[850,9019,9020],{"class":1667},"createHash",[850,9022,1766],{"class":856},[850,9024,9025],{"class":877},"\"sha1\"",[850,9027,6029],{"class":856},[850,9029,7494],{"class":1667},[850,9031,9032],{"class":856},"(password).",[850,9034,9035],{"class":1667},"toUpperCase",[850,9037,6771],{"class":856},[850,9039,7500],{"class":1667},[850,9041,1766],{"class":856},[850,9043,7466],{"class":877},[850,9045,5999],{"class":856},[850,9047,9048,9050,9053,9055,9058,9061,9063,9065,9067,9069],{"class":852,"line":240},[850,9049,1737],{"class":1660},[850,9051,9052],{"class":862}," prefix",[850,9054,1743],{"class":1660},[850,9056,9057],{"class":856}," hash.",[850,9059,9060],{"class":1667},"slice",[850,9062,1766],{"class":856},[850,9064,4039],{"class":862},[850,9066,797],{"class":856},[850,9068,5468],{"class":862},[850,9070,5999],{"class":856},[850,9072,9073,9075,9078,9080,9082,9084,9086,9088],{"class":852,"line":884},[850,9074,1737],{"class":1660},[850,9076,9077],{"class":862}," suffix",[850,9079,1743],{"class":1660},[850,9081,9057],{"class":856},[850,9083,9060],{"class":1667},[850,9085,1766],{"class":856},[850,9087,5468],{"class":862},[850,9089,5999],{"class":856},[850,9091,9092],{"class":852,"line":897},[850,9093,2650],{"emptyLinePlaceholder":266},[850,9095,9096,9098,9100,9102,9104,9106,9108,9111,9114,9116],{"class":852,"line":910},[850,9097,1737],{"class":1660},[850,9099,3157],{"class":862},[850,9101,1743],{"class":1660},[850,9103,1746],{"class":1660},[850,9105,3164],{"class":1667},[850,9107,1766],{"class":856},[850,9109,9110],{"class":877},"`https://api.pwnedpasswords.com/range/${",[850,9112,9113],{"class":856},"prefix",[850,9115,3912],{"class":877},[850,9117,5999],{"class":856},[850,9119,9120,9122,9125,9127,9129,9132,9134],{"class":852,"line":921},[850,9121,1737],{"class":1660},[850,9123,9124],{"class":862}," text",[850,9126,1743],{"class":1660},[850,9128,1746],{"class":1660},[850,9130,9131],{"class":856}," response.",[850,9133,739],{"class":1667},[850,9135,6113],{"class":856},[850,9137,9138],{"class":852,"line":268},[850,9139,2650],{"emptyLinePlaceholder":266},[850,9141,9142,9144,9147,9149,9151,9154,9157,9159,9161,9164,9166,9168,9170,9172,9175,9178],{"class":852,"line":1338},[850,9143,1757],{"class":1660},[850,9145,9146],{"class":856}," text.",[850,9148,7278],{"class":1667},[850,9150,1766],{"class":856},[850,9152,9153],{"class":877},"\"",[850,9155,9156],{"class":862},"\\r\\n",[850,9158,9153],{"class":877},[850,9160,6029],{"class":856},[850,9162,9163],{"class":1667},"some",[850,9165,3338],{"class":856},[850,9167,852],{"class":1676},[850,9169,2591],{"class":856},[850,9171,2594],{"class":1660},[850,9173,9174],{"class":856}," line.",[850,9176,9177],{"class":1667},"startsWith",[850,9179,9180],{"class":856},"(suffix));\n",[850,9182,9183],{"class":852,"line":1495},[850,9184,929],{"class":856},[20,9186,9187],{},"Block passwords that appear in breach databases. A password from a leaked database is likely in every attacker's wordlist.",[15,9189,9191],{"id":9190},"session-management","Session Management",[20,9193,9194],{},"Session tokens are the credentials your application issues after successful authentication. They need the same care as passwords.",[20,9196,9197],{},"Generate session tokens with cryptographically secure randomness:",[734,9199,9201],{"className":1646,"code":9200,"language":1648,"meta":239,"style":239},"import { randomBytes } from \"crypto\";\n\nFunction generateSessionToken(): string {\n return randomBytes(32).toString(\"hex\"); // 256 bits of entropy\n}\n",[741,9202,9203,9216,9220,9230,9255],{"__ignoreMap":239},[850,9204,9205,9207,9210,9212,9214],{"class":852,"line":853},[850,9206,3546],{"class":1660},[850,9208,9209],{"class":856}," { randomBytes } ",[850,9211,3552],{"class":1660},[850,9213,7421],{"class":877},[850,9215,1896],{"class":856},[850,9217,9218],{"class":852,"line":243},[850,9219,2650],{"emptyLinePlaceholder":266},[850,9221,9222,9224,9227],{"class":852,"line":240},[850,9223,7432],{"class":856},[850,9225,9226],{"class":1667},"generateSessionToken",[850,9228,9229],{"class":856},"(): string {\n",[850,9231,9232,9234,9237,9239,9241,9243,9245,9247,9249,9252],{"class":852,"line":884},[850,9233,1757],{"class":1660},[850,9235,9236],{"class":1667}," randomBytes",[850,9238,1766],{"class":856},[850,9240,7457],{"class":862},[850,9242,6029],{"class":856},[850,9244,2689],{"class":1667},[850,9246,1766],{"class":856},[850,9248,7466],{"class":877},[850,9250,9251],{"class":856},"); ",[850,9253,9254],{"class":1427},"// 256 bits of entropy\n",[850,9256,9257],{"class":852,"line":897},[850,9258,929],{"class":856},[20,9260,9261],{},"256 bits of entropy is not guessable. Compare this to a 4-digit PIN (10,000 possibilities) or a sequential session ID (trivially enumerable).",[20,9263,9264],{},"Set appropriate cookie attributes:",[734,9266,9268],{"className":1646,"code":9267,"language":1648,"meta":239,"style":239},"res.cookie(\"session\", token, {\n httpOnly: true, // Not accessible to JavaScript\n secure: true, // HTTPS only\n sameSite: \"lax\", // Prevents CSRF\n maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days\n path: \"/\",\n});\n",[741,9269,9270,9286,9298,9310,9323,9352,9362],{"__ignoreMap":239},[850,9271,9272,9275,9278,9280,9283],{"class":852,"line":853},[850,9273,9274],{"class":856},"res.",[850,9276,9277],{"class":1667},"cookie",[850,9279,1766],{"class":856},[850,9281,9282],{"class":877},"\"session\"",[850,9284,9285],{"class":856},", token, {\n",[850,9287,9288,9291,9293,9295],{"class":852,"line":243},[850,9289,9290],{"class":856}," httpOnly: ",[850,9292,1097],{"class":862},[850,9294,797],{"class":856},[850,9296,9297],{"class":1427},"// Not accessible to JavaScript\n",[850,9299,9300,9303,9305,9307],{"class":852,"line":240},[850,9301,9302],{"class":856}," secure: ",[850,9304,1097],{"class":862},[850,9306,797],{"class":856},[850,9308,9309],{"class":1427},"// HTTPS only\n",[850,9311,9312,9315,9318,9320],{"class":852,"line":884},[850,9313,9314],{"class":856}," sameSite: ",[850,9316,9317],{"class":877},"\"lax\"",[850,9319,797],{"class":856},[850,9321,9322],{"class":1427},"// Prevents CSRF\n",[850,9324,9325,9327,9330,9332,9335,9337,9339,9341,9343,9345,9347,9349],{"class":852,"line":897},[850,9326,7353],{"class":856},[850,9328,9329],{"class":862},"7",[850,9331,4806],{"class":1660},[850,9333,9334],{"class":862}," 24",[850,9336,4806],{"class":1660},[850,9338,4809],{"class":862},[850,9340,4806],{"class":1660},[850,9342,4809],{"class":862},[850,9344,4806],{"class":1660},[850,9346,4232],{"class":862},[850,9348,797],{"class":856},[850,9350,9351],{"class":1427},"// 7 days\n",[850,9353,9354,9357,9360],{"class":852,"line":910},[850,9355,9356],{"class":856}," path: ",[850,9358,9359],{"class":877},"\"/\"",[850,9361,881],{"class":856},[850,9363,9364],{"class":852,"line":921},[850,9365,6621],{"class":856},[20,9367,9368],{},"Implement session invalidation on logout. This is obvious but frequently done incorrectly. Clearing the cookie client-side without invalidating the server-side session record means the session is still valid — anyone with the token can continue using it.",[734,9370,9372],{"className":1646,"code":9371,"language":1648,"meta":239,"style":239},"async function logout(req: Request, res: Response): Promise\u003Cvoid> {\n const token = req.cookies.session;\n if (token) {\n await db.session.delete({ where: { token } }); // Invalidate server-side\n }\n res.clearCookie(\"session\");\n res.redirect(\"/login\");\n}\n",[741,9373,9374,9411,9422,9429,9444,9448,9461,9475],{"__ignoreMap":239},[850,9375,9376,9378,9380,9383,9385,9387,9389,9391,9393,9395,9397,9399,9401,9403,9405,9407,9409],{"class":852,"line":853},[850,9377,1661],{"class":1660},[850,9379,1664],{"class":1660},[850,9381,9382],{"class":1667}," logout",[850,9384,1766],{"class":856},[850,9386,6341],{"class":1676},[850,9388,1680],{"class":1660},[850,9390,5933],{"class":1667},[850,9392,797],{"class":856},[850,9394,6350],{"class":1676},[850,9396,1680],{"class":1660},[850,9398,5945],{"class":1667},[850,9400,1718],{"class":856},[850,9402,1680],{"class":1660},[850,9404,1723],{"class":1667},[850,9406,1726],{"class":856},[850,9408,5969],{"class":862},[850,9410,1732],{"class":856},[850,9412,9413,9415,9417,9419],{"class":852,"line":243},[850,9414,1737],{"class":1660},[850,9416,5978],{"class":862},[850,9418,1743],{"class":1660},[850,9420,9421],{"class":856}," req.cookies.session;\n",[850,9423,9424,9426],{"class":852,"line":240},[850,9425,2947],{"class":1660},[850,9427,9428],{"class":856}," (token) {\n",[850,9430,9431,9433,9436,9438,9441],{"class":852,"line":884},[850,9432,1746],{"class":1660},[850,9434,9435],{"class":856}," db.session.",[850,9437,6442],{"class":1667},[850,9439,9440],{"class":856},"({ where: { token } }); ",[850,9442,9443],{"class":1427},"// Invalidate server-side\n",[850,9445,9446],{"class":852,"line":897},[850,9447,924],{"class":856},[850,9449,9450,9452,9455,9457,9459],{"class":852,"line":910},[850,9451,6019],{"class":856},[850,9453,9454],{"class":1667},"clearCookie",[850,9456,1766],{"class":856},[850,9458,9282],{"class":877},[850,9460,5999],{"class":856},[850,9462,9463,9465,9468,9470,9473],{"class":852,"line":921},[850,9464,6019],{"class":856},[850,9466,9467],{"class":1667},"redirect",[850,9469,1766],{"class":856},[850,9471,9472],{"class":877},"\"/login\"",[850,9474,5999],{"class":856},[850,9476,9477],{"class":852,"line":268},[850,9478,929],{"class":856},[15,9480,9482],{"id":9481},"rate-limiting-authentication-endpoints","Rate Limiting Authentication Endpoints",[20,9484,9485],{},"Authentication endpoints without rate limiting are vulnerable to brute-force attacks. With a weak password policy or common passwords, an attacker making thousands of login attempts can eventually authenticate.",[20,9487,9488],{},"Rate limit at multiple levels:",[20,9490,9491,9494],{},[123,9492,9493],{},"Per-IP rate limit"," — limit login attempts from a single IP. 10 attempts per 15 minutes is a reasonable starting point. Implement with exponential backoff for repeated failures.",[20,9496,9497,9500],{},[123,9498,9499],{},"Per-account rate limit"," — limit login attempts against a specific account. 5 failed attempts trigger a lockout period. This prevents targeted attacks where an attacker rotates through multiple IPs to evade per-IP limits.",[20,9502,9503,9506],{},[123,9504,9505],{},"Global rate limit"," — a sharp increase in global login attempts (credential stuffing attack) should trigger additional scrutiny or temporary CAPTCHA enforcement.",[734,9508,9510],{"className":1646,"code":9509,"language":1648,"meta":239,"style":239},"const loginLimiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 10,\n keyGenerator: (req) => req.ip + \":\" + req.body.email, // Per IP + per account\n message: { error: \"Too many login attempts, try again in 15 minutes\" },\n});\n",[741,9511,9512,9525,9541,9549,9578,9587],{"__ignoreMap":239},[850,9513,9514,9516,9519,9521,9523],{"class":852,"line":853},[850,9515,3123],{"class":1660},[850,9517,9518],{"class":862}," loginLimiter",[850,9520,1743],{"class":1660},[850,9522,3628],{"class":1667},[850,9524,2780],{"class":856},[850,9526,9527,9529,9531,9533,9535,9537,9539],{"class":852,"line":243},[850,9528,4800],{"class":856},[850,9530,4803],{"class":862},[850,9532,4806],{"class":1660},[850,9534,4809],{"class":862},[850,9536,4806],{"class":1660},[850,9538,4232],{"class":862},[850,9540,881],{"class":856},[850,9542,9543,9545,9547],{"class":852,"line":240},[850,9544,6577],{"class":856},[850,9546,4863],{"class":862},[850,9548,881],{"class":856},[850,9550,9551,9553,9555,9557,9559,9561,9564,9566,9569,9572,9575],{"class":852,"line":884},[850,9552,4387],{"class":1667},[850,9554,4958],{"class":856},[850,9556,6341],{"class":1676},[850,9558,2591],{"class":856},[850,9560,2594],{"class":1660},[850,9562,9563],{"class":856}," req.ip ",[850,9565,4156],{"class":1660},[850,9567,9568],{"class":877}," \":\"",[850,9570,9571],{"class":1660}," +",[850,9573,9574],{"class":856}," req.body.email, ",[850,9576,9577],{"class":1427},"// Per IP + per account\n",[850,9579,9580,9582,9585],{"class":852,"line":897},[850,9581,6602],{"class":856},[850,9583,9584],{"class":877},"\"Too many login attempts, try again in 15 minutes\"",[850,9586,4720],{"class":856},[850,9588,9589],{"class":852,"line":910},[850,9590,6621],{"class":856},[15,9592,9594],{"id":9593},"multi-factor-authentication","Multi-Factor Authentication",[20,9596,9597],{},"MFA dramatically reduces the risk of compromised passwords. Even if an attacker has a user's password (from a breach, phishing, or brute force), they cannot authenticate without the second factor.",[20,9599,9600],{},"TOTP (Time-based One-Time Passwords) is the most widely supported MFA method. Google Authenticator, Authy, and most password managers support it:",[734,9602,9604],{"className":1646,"code":9603,"language":1648,"meta":239,"style":239},"import { authenticator } from \"otplib\";\n\n// Generate a secret for a user during MFA setup\nfunction generateMfaSecret(): string {\n return authenticator.generateSecret();\n}\n\n// Generate a QR code URL for scanning with authenticator app\nfunction getMfaQrUrl(email: string, secret: string): string {\n return authenticator.keyuri(email, \"YourApp\", secret);\n}\n\n// Verify a TOTP code\nfunction verifyMfaCode(token: string, secret: string): boolean {\n return authenticator.check(token, secret);\n}\n",[741,9605,9606,9620,9624,9629,9644,9656,9660,9664,9669,9702,9720,9724,9728,9733,9766,9778],{"__ignoreMap":239},[850,9607,9608,9610,9613,9615,9618],{"class":852,"line":853},[850,9609,3546],{"class":1660},[850,9611,9612],{"class":856}," { authenticator } ",[850,9614,3552],{"class":1660},[850,9616,9617],{"class":877}," \"otplib\"",[850,9619,1896],{"class":856},[850,9621,9622],{"class":852,"line":243},[850,9623,2650],{"emptyLinePlaceholder":266},[850,9625,9626],{"class":852,"line":240},[850,9627,9628],{"class":1427},"// Generate a secret for a user during MFA setup\n",[850,9630,9631,9633,9636,9638,9640,9642],{"class":852,"line":884},[850,9632,4345],{"class":1660},[850,9634,9635],{"class":1667}," generateMfaSecret",[850,9637,3909],{"class":856},[850,9639,1680],{"class":1660},[850,9641,1683],{"class":862},[850,9643,1849],{"class":856},[850,9645,9646,9648,9651,9654],{"class":852,"line":897},[850,9647,1757],{"class":1660},[850,9649,9650],{"class":856}," authenticator.",[850,9652,9653],{"class":1667},"generateSecret",[850,9655,6113],{"class":856},[850,9657,9658],{"class":852,"line":910},[850,9659,929],{"class":856},[850,9661,9662],{"class":852,"line":921},[850,9663,2650],{"emptyLinePlaceholder":266},[850,9665,9666],{"class":852,"line":268},[850,9667,9668],{"class":1427},"// Generate a QR code URL for scanning with authenticator app\n",[850,9670,9671,9673,9676,9678,9681,9683,9685,9687,9690,9692,9694,9696,9698,9700],{"class":852,"line":1338},[850,9672,4345],{"class":1660},[850,9674,9675],{"class":1667}," getMfaQrUrl",[850,9677,1766],{"class":856},[850,9679,9680],{"class":1676},"email",[850,9682,1680],{"class":1660},[850,9684,1683],{"class":862},[850,9686,797],{"class":856},[850,9688,9689],{"class":1676},"secret",[850,9691,1680],{"class":1660},[850,9693,1683],{"class":862},[850,9695,1718],{"class":856},[850,9697,1680],{"class":1660},[850,9699,1683],{"class":862},[850,9701,1849],{"class":856},[850,9703,9704,9706,9708,9711,9714,9717],{"class":852,"line":1495},[850,9705,1757],{"class":1660},[850,9707,9650],{"class":856},[850,9709,9710],{"class":1667},"keyuri",[850,9712,9713],{"class":856},"(email, ",[850,9715,9716],{"class":877},"\"YourApp\"",[850,9718,9719],{"class":856},", secret);\n",[850,9721,9722],{"class":852,"line":1503},[850,9723,929],{"class":856},[850,9725,9726],{"class":852,"line":1514},[850,9727,2650],{"emptyLinePlaceholder":266},[850,9729,9730],{"class":852,"line":1522},[850,9731,9732],{"class":1427},"// Verify a TOTP code\n",[850,9734,9735,9737,9740,9742,9745,9747,9749,9751,9753,9755,9757,9759,9761,9764],{"class":852,"line":1530},[850,9736,4345],{"class":1660},[850,9738,9739],{"class":1667}," verifyMfaCode",[850,9741,1766],{"class":856},[850,9743,9744],{"class":1676},"token",[850,9746,1680],{"class":1660},[850,9748,1683],{"class":862},[850,9750,797],{"class":856},[850,9752,9689],{"class":1676},[850,9754,1680],{"class":1660},[850,9756,1683],{"class":862},[850,9758,1718],{"class":856},[850,9760,1680],{"class":1660},[850,9762,9763],{"class":862}," boolean",[850,9765,1849],{"class":856},[850,9767,9768,9770,9772,9775],{"class":852,"line":1541},[850,9769,1757],{"class":1660},[850,9771,9650],{"class":856},[850,9773,9774],{"class":1667},"check",[850,9776,9777],{"class":856},"(token, secret);\n",[850,9779,9780],{"class":852,"line":1548},[850,9781,929],{"class":856},[20,9783,9784],{},"Passkeys (WebAuthn) are the best authentication mechanism available in 2026. They are phishing-resistant (credentials are domain-bound), do not require passwords, and are backed by hardware (the user's device biometrics or PIN). Major browsers and platforms support them. If you are building a new application, implementing passkeys alongside traditional auth is worth the investment.",[15,9786,9788],{"id":9787},"password-reset-security","Password Reset Security",[20,9790,9791],{},"Password reset flows are a common attack vector. The secure implementation:",[9793,9794,9795,9798,9801,9804,9807,9810],"ol",{},[214,9796,9797],{},"User submits email",[214,9799,9800],{},"If the email exists, generate a cryptographically random, single-use token with short expiry (15-30 minutes)",[214,9802,9803],{},"Send a reset link containing the token to the email address",[214,9805,9806],{},"On link click, validate the token is valid and unexpired",[214,9808,9809],{},"Accept the new password, hash it, update the user record, invalidate the token",[214,9811,9812],{},"Invalidate all existing sessions for the user",[20,9814,9815],{},"Critical details:",[211,9817,9818,9821,9824],{},[214,9819,9820],{},"Do not reveal whether an email address exists in your system. Return the same response regardless of whether the email is registered (\"If an account exists with this email, you will receive a reset link\"). This prevents user enumeration.",[214,9822,9823],{},"Invalidate the token immediately after use — do not allow replaying the same reset link.",[214,9825,9826],{},"Log the reset event including the IP and timestamp for security audit.",[734,9828,9830],{"className":1646,"code":9829,"language":1648,"meta":239,"style":239},"async function initiatePasswordReset(email: string): Promise\u003Cvoid> {\n const user = await db.user.findByEmail(email);\n\n // Always return success to prevent user enumeration\n if (!user) return;\n\n const token = randomBytes(32).toString(\"hex\");\n const expiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes\n\n await db.passwordReset.upsert({\n where: { userId: user.id },\n update: { token, expiresAt: expiry },\n create: { userId: user.id, token, expiresAt: expiry },\n });\n\n await emailService.sendPasswordReset(user.email, token);\n}\n",[741,9831,9832,9861,9879,9883,9888,9902,9906,9930,9968,9972,9984,9989,9994,9999,10003,10007,10020],{"__ignoreMap":239},[850,9833,9834,9836,9838,9841,9843,9845,9847,9849,9851,9853,9855,9857,9859],{"class":852,"line":853},[850,9835,1661],{"class":1660},[850,9837,1664],{"class":1660},[850,9839,9840],{"class":1667}," initiatePasswordReset",[850,9842,1766],{"class":856},[850,9844,9680],{"class":1676},[850,9846,1680],{"class":1660},[850,9848,1683],{"class":862},[850,9850,1718],{"class":856},[850,9852,1680],{"class":1660},[850,9854,1723],{"class":1667},[850,9856,1726],{"class":856},[850,9858,5969],{"class":862},[850,9860,1732],{"class":856},[850,9862,9863,9865,9867,9869,9871,9873,9876],{"class":852,"line":243},[850,9864,1737],{"class":1660},[850,9866,3196],{"class":862},[850,9868,1743],{"class":1660},[850,9870,1746],{"class":1660},[850,9872,7159],{"class":856},[850,9874,9875],{"class":1667},"findByEmail",[850,9877,9878],{"class":856},"(email);\n",[850,9880,9881],{"class":852,"line":240},[850,9882,2650],{"emptyLinePlaceholder":266},[850,9884,9885],{"class":852,"line":884},[850,9886,9887],{"class":1427}," // Always return success to prevent user enumeration\n",[850,9889,9890,9892,9894,9896,9898,9900],{"class":852,"line":897},[850,9891,2947],{"class":1660},[850,9893,1123],{"class":856},[850,9895,4068],{"class":1660},[850,9897,7175],{"class":856},[850,9899,2953],{"class":1660},[850,9901,1896],{"class":856},[850,9903,9904],{"class":852,"line":910},[850,9905,2650],{"emptyLinePlaceholder":266},[850,9907,9908,9910,9912,9914,9916,9918,9920,9922,9924,9926,9928],{"class":852,"line":921},[850,9909,1737],{"class":1660},[850,9911,5978],{"class":862},[850,9913,1743],{"class":1660},[850,9915,9236],{"class":1667},[850,9917,1766],{"class":856},[850,9919,7457],{"class":862},[850,9921,6029],{"class":856},[850,9923,2689],{"class":1667},[850,9925,1766],{"class":856},[850,9927,7466],{"class":877},[850,9929,5999],{"class":856},[850,9931,9932,9934,9937,9939,9941,9943,9946,9948,9950,9952,9955,9957,9959,9961,9963,9965],{"class":852,"line":268},[850,9933,1737],{"class":1660},[850,9935,9936],{"class":862}," expiry",[850,9938,1743],{"class":1660},[850,9940,3131],{"class":1660},[850,9942,4150],{"class":1667},[850,9944,9945],{"class":856},"(Date.",[850,9947,2611],{"class":1667},[850,9949,2639],{"class":856},[850,9951,4156],{"class":1660},[850,9953,9954],{"class":862}," 30",[850,9956,4806],{"class":1660},[850,9958,4809],{"class":862},[850,9960,4806],{"class":1660},[850,9962,4232],{"class":862},[850,9964,9251],{"class":856},[850,9966,9967],{"class":1427},"// 30 minutes\n",[850,9969,9970],{"class":852,"line":1338},[850,9971,2650],{"emptyLinePlaceholder":266},[850,9973,9974,9976,9979,9982],{"class":852,"line":1495},[850,9975,1746],{"class":1660},[850,9977,9978],{"class":856}," db.passwordReset.",[850,9980,9981],{"class":1667},"upsert",[850,9983,2780],{"class":856},[850,9985,9986],{"class":852,"line":1503},[850,9987,9988],{"class":856}," where: { userId: user.id },\n",[850,9990,9991],{"class":852,"line":1514},[850,9992,9993],{"class":856}," update: { token, expiresAt: expiry },\n",[850,9995,9996],{"class":852,"line":1522},[850,9997,9998],{"class":856}," create: { userId: user.id, token, expiresAt: expiry },\n",[850,10000,10001],{"class":852,"line":1530},[850,10002,6039],{"class":856},[850,10004,10005],{"class":852,"line":1541},[850,10006,2650],{"emptyLinePlaceholder":266},[850,10008,10009,10011,10014,10017],{"class":852,"line":1548},[850,10010,1746],{"class":1660},[850,10012,10013],{"class":856}," emailService.",[850,10015,10016],{"class":1667},"sendPasswordReset",[850,10018,10019],{"class":856},"(user.email, token);\n",[850,10021,10022],{"class":852,"line":1555},[850,10023,929],{"class":856},[15,10025,10027],{"id":10026},"the-authentication-security-checklist","The Authentication Security Checklist",[20,10029,10030],{},"Before your first user logs in:",[211,10032,10033,10036,10039,10042,10045,10048,10051,10054,10057],{},[214,10034,10035],{},"Passwords hashed with bcrypt (cost 12+) or Argon2id",[214,10037,10038],{},"Passwords checked against breach databases on creation and change",[214,10040,10041],{},"Session tokens 256 bits of entropy minimum",[214,10043,10044],{},"Sessions invalidated on logout (server-side)",[214,10046,10047],{},"Rate limiting on login and password reset endpoints",[214,10049,10050],{},"MFA available (TOTP at minimum, passkeys preferred)",[214,10052,10053],{},"Password reset tokens single-use, short-lived, randomly generated",[214,10055,10056],{},"Account lockout after repeated failures with recovery path",[214,10058,10059],{},"Auth events logged (login, logout, failed login, password change, MFA change)",[30,10061],{},[20,10063,10064,10065,778],{},"If you want a review of your authentication implementation or help building a secure auth system from scratch, book a session at ",[197,10066,199],{"href":199,"rel":10067},[201],[30,10069],{},[15,10071,209],{"id":208},[211,10073,10074,10078,10082,10086],{},[214,10075,10076],{},[197,10077,5862],{"href":7732},[214,10079,10080],{},[197,10081,7712],{"href":7711},[214,10083,10084],{},[197,10085,7694],{"href":7693},[214,10087,10088],{},[197,10089,10091],{"href":10090},"/blog/security-headers-web-apps","Security Headers for Web Applications: The Complete Configuration Guide",[1309,10093,10094],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":239,"searchDepth":240,"depth":240,"links":10096},[10097,10098,10099,10100,10101,10102,10103,10104],{"id":8606,"depth":243,"text":8607},{"id":8948,"depth":243,"text":8949},{"id":9190,"depth":243,"text":9191},{"id":9481,"depth":243,"text":9482},{"id":9593,"depth":243,"text":9594},{"id":9787,"depth":243,"text":9788},{"id":10026,"depth":243,"text":10027},{"id":208,"depth":243,"text":209},"Authentication security fundamentals for web applications — password hashing, session management, MFA implementation, account lockout, and passkeys in 2026.",[10107,10108],"authentication security","login security",{},{"title":7706,"description":10105},"blog/authentication-security-guide",[10113,5858,10114,10115],"Authentication","Login Security","Sessions","qt3oPlfXPJUfAvbrrNUJG6w8ysTA9CoY3BEVdTEs_bA",{"id":10118,"title":225,"author":10119,"body":10120,"category":256,"date":257,"description":10365,"extension":259,"featured":260,"image":261,"keywords":10366,"meta":10369,"navigation":266,"path":224,"readTime":268,"seo":10370,"stem":10371,"tags":10372,"__hash__":10375},"blog/blog/automated-testing-with-ai.md",{"name":9,"bio":10},{"type":12,"value":10121,"toc":10347},[10122,10126,10129,10132,10134,10138,10142,10145,10162,10165,10168,10172,10175,10178,10182,10185,10188,10192,10195,10198,10200,10204,10207,10210,10224,10227,10229,10233,10236,10239,10245,10251,10253,10257,10261,10264,10267,10271,10274,10277,10281,10284,10287,10289,10293,10296,10299,10302,10305,10308,10311,10314,10317,10325,10327,10329],[15,10123,10125],{"id":10124},"testing-is-the-unglamorous-work-that-determines-quality","Testing Is the Unglamorous Work That Determines Quality",[20,10127,10128],{},"Nobody talks about testing the way they talk about AI features. Testing doesn't make demos impressive. It doesn't get written up as a competitive advantage. It's the work that happens between writing code and shipping it, and in many development practices, it's the work that gets cut when timelines compress.",[20,10130,10131],{},"AI tools haven't made testing glamorous. What they have done is removed some of the most tedious friction from testing work and enabled better coverage than was practical before. I want to be specific about what that looks like and where the limits are.",[30,10133],{},[15,10135,10137],{"id":10136},"what-ai-does-well-in-the-testing-workflow","What AI Does Well in the Testing Workflow",[52,10139,10141],{"id":10140},"unit-test-generation","Unit Test Generation",[20,10143,10144],{},"This is the clearest win. Given a function or method, AI tools generate comprehensive unit tests faster than a developer would write them manually. The generated tests typically cover:",[211,10146,10147,10150,10153,10156,10159],{},[214,10148,10149],{},"The happy path (expected inputs produce expected outputs)",[214,10151,10152],{},"Null/undefined inputs",[214,10154,10155],{},"Boundary conditions (empty arrays, zero values, maximum values)",[214,10157,10158],{},"Type edge cases",[214,10160,10161],{},"Error conditions",[20,10163,10164],{},"The quality is good enough that I use AI-generated tests as a starting point rather than writing from scratch. I review them, add cases the AI missed, and occasionally remove cases that test the wrong thing. But the 80% that's correct saves real time.",[20,10166,10167],{},"One important caveat: AI-generated unit tests test behavior based on how the code looks, not necessarily on what the code is supposed to do. A function with a bug will get tests that confirm the buggy behavior. Generated tests are not a substitute for design-time thinking about what correct behavior looks like — they're a mechanical way to encode that behavior once you know what it should be.",[52,10169,10171],{"id":10170},"test-case-expansion-for-edge-cases","Test Case Expansion for Edge Cases",[20,10173,10174],{},"A specific use I've found valuable: giving AI a test suite I've written and asking it to identify edge cases I might have missed. It consistently surfaces cases I didn't consider — unusual character encoding in string inputs, very large numbers, date edge cases (end of month, leap year), concurrent modification scenarios.",[20,10176,10177],{},"This is a different use than generating tests from scratch. It's using AI as a second reviewer of my test design, which is a genuinely different perspective and catches different things than a human reviewer would.",[52,10179,10181],{"id":10180},"integration-test-scaffolding","Integration Test Scaffolding",[20,10183,10184],{},"Integration tests require more setup than unit tests — database fixtures, mocked services, HTTP clients. The setup code is tedious and repetitive. AI generates integration test scaffolding well, including the setup/teardown patterns, fixture generation, and assertion helper code.",[20,10186,10187],{},"The business logic of what to test still requires human judgment. The mechanical scaffolding around that logic can be largely generated.",[52,10189,10191],{"id":10190},"end-to-end-test-generation-from-requirements","End-to-End Test Generation from Requirements",[20,10193,10194],{},"For UI-level E2E tests (Playwright, Cypress), giving AI a feature description or user story and asking it to generate test scenarios produces useful starting points. It translates requirements into test cases in a systematic way that catches cases a developer writing tests from memory might miss.",[20,10196,10197],{},"The generated E2E tests require review for selector stability (AI-generated selectors aren't always maintainable) and for test design (AI tends toward brittle tests that check too many things in one scenario). But as starting points, they're faster than writing from scratch.",[30,10199],{},[15,10201,10203],{"id":10202},"ai-for-test-coverage-analysis","AI for Test Coverage Analysis",[20,10205,10206],{},"Traditional coverage metrics (line coverage, branch coverage) tell you whether code was executed during tests. They don't tell you whether the important behaviors were tested. An application can have 95% line coverage and still be missing tests for the scenarios that actually fail in production.",[20,10208,10209],{},"AI-assisted coverage analysis goes beyond line coverage to ask: what behavioral scenarios are not covered by the existing tests? Given a module and its test suite, AI can identify:",[211,10211,10212,10215,10218,10221],{},[214,10213,10214],{},"Input combinations that tests don't exercise",[214,10216,10217],{},"State transitions not covered by existing tests",[214,10219,10220],{},"Error handling paths that have no test coverage",[214,10222,10223],{},"Business logic branches that aren't directly tested",[20,10225,10226],{},"This qualitative coverage analysis is more useful than quantitative coverage metrics for identifying where testing effort should be focused.",[30,10228],{},[15,10230,10232],{"id":10231},"ai-for-test-maintenance","AI for Test Maintenance",[20,10234,10235],{},"One of the most time-intensive aspects of automated testing is maintenance — updating tests when the code changes. When a component is refactored, when an API changes, when business logic is updated, tests need to update with it.",[20,10237,10238],{},"AI tools help here in two ways:",[20,10240,10241,10244],{},[123,10242,10243],{},"Breaking change identification",": When reviewing a code change, AI can identify which existing tests are likely to break and why, before running the full test suite. This is faster feedback than waiting for CI to fail.",[20,10246,10247,10250],{},[123,10248,10249],{},"Test update assistance",": When tests do break due to intentional code changes, AI can suggest test updates that align the tests with the new behavior. This is faster than manual test rewriting, particularly for tests where the change is mechanical (new API signature, renamed method, restructured response).",[30,10252],{},[15,10254,10256],{"id":10255},"where-ai-testing-tools-fall-short","Where AI Testing Tools Fall Short",[52,10258,10260],{"id":10259},"high-stakes-business-logic","High-Stakes Business Logic",[20,10262,10263],{},"For complex business logic — tax calculations, financial computations, legal rule application, medical decision support — AI-generated tests are not sufficient. These domains require tests designed by someone who understands the business requirements, edge cases specific to the domain, and the regulatory requirements for correctness.",[20,10265,10266],{},"AI generates structurally plausible tests. It doesn't know that your Texas clients have a different sales tax treatment for SaaS subscriptions, or that the validation rule has an exception for accounts created before a specific date. Domain knowledge is irreplaceable for business logic testing.",[52,10268,10270],{"id":10269},"security-testing","Security Testing",[20,10272,10273],{},"AI-assisted testing does not adequately cover security. Generating functional unit tests for an authentication function is different from testing that function for authentication bypass vulnerabilities. Security testing requires specific security expertise, adversarial thinking, and knowledge of vulnerability classes that goes well beyond what AI test generation provides.",[20,10275,10276],{},"Use AI to improve code coverage on security-sensitive components. Use dedicated security testing practices and tools for security assurance.",[52,10278,10280],{"id":10279},"performance-and-load-testing","Performance and Load Testing",[20,10282,10283],{},"AI doesn't help much with performance testing strategy. Determining what performance characteristics to test, what load patterns represent production reality, and what thresholds represent acceptable performance requires knowledge of the system's usage patterns and business requirements that AI tools don't have.",[20,10285,10286],{},"AI can generate load test scripts from specifications, but specifying those requirements is the hard part.",[30,10288],{},[15,10290,10292],{"id":10291},"building-an-ai-enhanced-testing-practice","Building an AI-Enhanced Testing Practice",[20,10294,10295],{},"Here's the testing workflow I use in my practice:",[20,10297,10298],{},"Design-time: Write tests for critical business logic first, before implementation (TDD where it makes sense). This step is not AI-assisted — it requires thinking about what correct behavior means.",[20,10300,10301],{},"Implementation time: Use AI to generate unit tests for implemented functions, reviewing and augmenting the generated tests. Accept the 80% that's correct, add the cases AI missed.",[20,10303,10304],{},"Coverage review: After implementing a feature, use AI to analyze the test suite for coverage gaps. Add tests for identified gaps.",[20,10306,10307],{},"Integration and E2E: Use AI to scaffold integration tests and generate E2E test scenarios from requirements. Review and refine generated tests for stability and correct assertion scope.",[20,10309,10310],{},"Maintenance: Use AI to identify and assist with test updates when code changes break existing tests.",[20,10312,10313],{},"This workflow doesn't eliminate testing judgment. It reduces the mechanical overhead of testing work so that developer time focuses on what requires human judgment: understanding what correct behavior looks like and designing tests that validate it.",[20,10315,10316],{},"The result is better coverage than would be practical without AI assistance, achieved in less time. That's the value proposition for AI in testing: not replacing testing judgment, but removing friction from the mechanical work so more testing can happen.",[20,10318,10319,10320,10324],{},"If you're building or improving a testing strategy for your development process and want a second opinion on how to integrate AI tools effectively, ",[197,10321,10323],{"href":199,"rel":10322},[201],"schedule a consultation at Calendly",". I can help you design an approach that improves coverage without adding workflow complexity.",[30,10326],{},[15,10328,209],{"id":208},[211,10330,10331,10335,10339,10343],{},[214,10332,10333],{},[197,10334,7],{"href":267},[214,10336,10337],{},[197,10338,279],{"href":493},[214,10340,10341],{},[197,10342,237],{"href":236},[214,10344,10345],{},[197,10346,231],{"href":230},{"title":239,"searchDepth":240,"depth":240,"links":10348},[10349,10350,10356,10357,10358,10363,10364],{"id":10124,"depth":243,"text":10125},{"id":10136,"depth":243,"text":10137,"children":10351},[10352,10353,10354,10355],{"id":10140,"depth":240,"text":10141},{"id":10170,"depth":240,"text":10171},{"id":10180,"depth":240,"text":10181},{"id":10190,"depth":240,"text":10191},{"id":10202,"depth":243,"text":10203},{"id":10231,"depth":243,"text":10232},{"id":10255,"depth":243,"text":10256,"children":10359},[10360,10361,10362],{"id":10259,"depth":240,"text":10260},{"id":10269,"depth":240,"text":10270},{"id":10279,"depth":240,"text":10280},{"id":10291,"depth":243,"text":10292},{"id":208,"depth":243,"text":209},"How AI tools are changing automated testing — from test generation to intelligent coverage analysis — and how to integrate them into a testing strategy that actually improves software quality.",[10367,10368],"automated testing AI","AI software development",{},{"title":225,"description":10365},"blog/automated-testing-with-ai",[10373,256,275,499,10374],"Testing","Development","rhMkBYB5hAEKouH5ROFeELtu4f-1EP2ObF1zEaaVILI",{"id":10377,"title":10378,"author":10379,"body":10380,"category":2154,"date":257,"description":10778,"extension":259,"featured":260,"image":261,"keywords":10779,"meta":10782,"navigation":266,"path":10783,"readTime":921,"seo":10784,"stem":10785,"tags":10786,"__hash__":10788},"blog/blog/b2b-saas-development.md","B2B SaaS Development: What's Different About Building for Businesses",{"name":9,"bio":10},{"type":12,"value":10381,"toc":10766},[10382,10386,10389,10392,10395,10397,10401,10404,10410,10417,10424,10426,10430,10433,10439,10453,10459,10469,10471,10475,10478,10481,10484,10508,10511,10513,10517,10520,10523,10526,10528,10532,10535,10538,10574,10577,10580,10582,10586,10589,10592,10595,10597,10601,10604,10610,10630,10635,10652,10655,10657,10661,10664,10725,10728,10730,10736,10738,10740,10764],[15,10383,10385],{"id":10384},"b2b-is-not-b2c-with-a-higher-price","B2B Is Not B2C With a Higher Price",[20,10387,10388],{},"Building software for businesses is a qualitatively different engineering problem from building software for consumers. The features that enterprise buyers require — SSO, audit logs, role-based access control, admin interfaces, compliance exports — are not premium add-ons. They're table stakes for deals above a certain contract size.",[20,10390,10391],{},"Founders who build a consumer-style product and try to sell it upmarket consistently run into the same wall: the product is technically sound but organizationally unready for enterprise procurement. IT departments require SSO. Legal requires data residency or export. Security requires audit logs. Compliance requires specific data retention policies. None of these were in the original roadmap.",[20,10393,10394],{},"The architects who build for enterprise from day one avoid this expensive retrofit. Here's what to build.",[30,10396],{},[15,10398,10400],{"id":10399},"the-organization-data-model","The Organization Data Model",[20,10402,10403],{},"B2B SaaS sells to organizations, not individuals. Your data model needs to represent this accurately from the start.",[734,10405,10408],{"className":10406,"code":10407,"language":739},[737],"users -- individuals who log in\norganizations -- the business entity that pays\norganization_members -- many-to-many: users belong to organizations with roles\ninvitations -- pending invitations to join an organization\n",[741,10409,10407],{"__ignoreMap":239},[20,10411,10412,10413,10416],{},"The ",[741,10414,10415],{},"organization_members"," table is where roles live. A user can be an admin in one organization and a viewer in another. Don't put the role on the user — put it on the membership.",[20,10418,10419,10420,10423],{},"Every resource in your system belongs to an organization. Projects, data, settings, configurations — all scoped by ",[741,10421,10422],{},"organization_id",". This is your multi-tenancy layer, and it needs to be consistent from the first migration.",[30,10425],{},[15,10427,10429],{"id":10428},"role-based-access-control-rbac","Role-Based Access Control (RBAC)",[20,10431,10432],{},"Enterprise products need more than \"admin\" and \"member.\" A typical B2B SaaS needs:",[20,10434,10435,10438],{},[123,10436,10437],{},"Predefined roles"," with a clear permission hierarchy:",[211,10440,10441,10444,10447,10450],{},[214,10442,10443],{},"Owner: full access, can delete the organization, can transfer ownership",[214,10445,10446],{},"Admin: full access except deleting the organization or transferring ownership",[214,10448,10449],{},"Member: standard access to core product features",[214,10451,10452],{},"Viewer: read-only access",[20,10454,10455,10458],{},[123,10456,10457],{},"Resource-level permissions"," for finer control: \"can edit this specific project,\" \"can view this dashboard but not others.\" If your product has resources that different users should have different access to, you need a permission system that goes below the role level.",[20,10460,10461,10464,10465,10468],{},[123,10462,10463],{},"Audit-safe permission checks."," Every permission check in your application should be explicit, logged (for audit purposes), and centralized. Don't scatter ",[741,10466,10467],{},"if (user.role === 'admin')"," checks throughout the codebase — use a centralized permission module that provides a single source of truth for access rules.",[30,10470],{},[15,10472,10474],{"id":10473},"single-sign-on-sso","Single Sign-On (SSO)",[20,10476,10477],{},"SSO is the single most common enterprise sales blocker I see. A company using Okta, Azure AD, or Google Workspace needs your product to support SSO before their IT department will allow it to be adopted company-wide.",[20,10479,10480],{},"The protocol standard is SAML 2.0 for enterprise IT environments and OIDC (OpenID Connect) for more modern organizations. Support both.",[20,10482,10483],{},"Building SSO from scratch is painful. Use a library or service:",[211,10485,10486,10492,10498],{},[214,10487,10488,10491],{},[123,10489,10490],{},"BoxyHQ"," (open source, self-hostable) is excellent for SAML",[214,10493,10494,10497],{},[123,10495,10496],{},"WorkOS"," is a managed service that handles SSO, Directory Sync, and Audit Logs",[214,10499,10500,10503,10504,10507],{},[123,10501,10502],{},"Auth0"," and ",[123,10505,10506],{},"Okta"," offer enterprise SSO features in their authentication platforms",[20,10509,10510],{},"The key configuration requirement: each organization should be able to configure their own SSO connection through a self-serve interface in their admin panel. You should not be manually setting up SSO connections for each customer — that doesn't scale.",[30,10512],{},[15,10514,10516],{"id":10515},"directory-sync-scim","Directory Sync (SCIM)",[20,10518,10519],{},"Beyond SSO, larger enterprises want automated user provisioning. When an employee joins the company, they should automatically get access to your product based on their role in the directory. When they leave, their account should be automatically deprovisioned.",[20,10521,10522],{},"SCIM (System for Cross-domain Identity Management) is the protocol that handles this. Enterprise buyers using Okta, Azure AD, or similar identity providers will expect SCIM support if they're deploying your product company-wide.",[20,10524,10525],{},"This is a later-stage feature — most B2B SaaS doesn't need it until they're pushing into enterprise contracts above $20-50K ACV. But know it's coming and don't build an architecture that makes it hard to add.",[30,10527],{},[15,10529,10531],{"id":10530},"audit-logs","Audit Logs",[20,10533,10534],{},"An audit log is a tamper-evident record of every significant action taken in the system: who did what, when, from which IP address. This is required for security compliance (SOC 2, ISO 27001), and enterprise buyers often ask for it in security questionnaires.",[20,10536,10537],{},"The data model:",[734,10539,10543],{"className":10540,"code":10541,"language":10542,"meta":239,"style":239},"language-sql shiki shiki-themes github-dark","audit_logs (\n id, organization_id, actor_id, actor_type,\n action, resource_type, resource_id,\n metadata (JSON), ip_address, user_agent,\n created_at\n)\n","sql",[741,10544,10545,10550,10555,10560,10565,10570],{"__ignoreMap":239},[850,10546,10547],{"class":852,"line":853},[850,10548,10549],{},"audit_logs (\n",[850,10551,10552],{"class":852,"line":243},[850,10553,10554],{}," id, organization_id, actor_id, actor_type,\n",[850,10556,10557],{"class":852,"line":240},[850,10558,10559],{}," action, resource_type, resource_id,\n",[850,10561,10562],{"class":852,"line":884},[850,10563,10564],{}," metadata (JSON), ip_address, user_agent,\n",[850,10566,10567],{"class":852,"line":897},[850,10568,10569],{}," created_at\n",[850,10571,10572],{"class":852,"line":910},[850,10573,1772],{},[20,10575,10576],{},"Actions to log: user invited, user role changed, user removed, settings changed, data exported, API key created/deleted, SSO configured. Any action that a security auditor would want to review.",[20,10578,10579],{},"Log to a separate, append-only table. Don't let application code delete audit log records. If you need to retain logs for a specific period and then purge, use a job that archives to cold storage before deletion.",[30,10581],{},[15,10583,10585],{"id":10584},"data-export-and-portability","Data Export and Portability",[20,10587,10588],{},"Enterprise customers want to be able to export their data. This is a legal and procurement requirement in many industries. Build a data export feature before you need it.",[20,10590,10591],{},"The minimum: a self-serve export in CSV or JSON of all data belonging to the organization. For more sensitive industries, you may need to support exports in specific formats (FHIR for healthcare, specific EDI formats for finance).",[20,10593,10594],{},"The export job should run asynchronously — don't try to generate a large export synchronously in a web request. Queue it, generate it in a background worker, and email a download link when it's ready.",[30,10596],{},[15,10598,10600],{"id":10599},"admin-interface-requirements","Admin Interface Requirements",[20,10602,10603],{},"Every B2B SaaS product needs two admin surfaces: one for your customers' admins (managing their organization) and one for your internal team (managing customers, handling support, investigating issues).",[20,10605,10606,10609],{},[123,10607,10608],{},"Customer admin panel"," features:",[211,10611,10612,10615,10618,10621,10624,10627],{},[214,10613,10614],{},"User management (invite, remove, change roles)",[214,10616,10617],{},"SSO configuration",[214,10619,10620],{},"Billing and subscription management",[214,10622,10623],{},"Audit log viewer",[214,10625,10626],{},"Data export",[214,10628,10629],{},"API key management",[20,10631,10632,10609],{},[123,10633,10634],{},"Internal admin panel",[211,10636,10637,10640,10643,10646,10649],{},[214,10638,10639],{},"Customer list with subscription status and key metrics",[214,10641,10642],{},"Ability to log in as a customer (impersonation, with audit logging)",[214,10644,10645],{},"Manual subscription adjustments",[214,10647,10648],{},"Feature flag overrides per customer",[214,10650,10651],{},"Support ticket linkage to accounts",[20,10653,10654],{},"Don't ship to enterprises without a functional customer admin panel. The company's IT admin will be the primary evaluator during procurement, and if they can't manage their users through a proper interface, the evaluation fails.",[30,10656],{},[15,10658,10660],{"id":10659},"the-enterprise-sales-feature-checklist","The Enterprise Sales Feature Checklist",[20,10662,10663],{},"Before you target enterprise accounts, verify:",[211,10665,10668,10677,10683,10689,10695,10701,10707,10713,10719],{"className":10666},[10667],"contains-task-list",[214,10669,10672,10676],{"className":10670},[10671],"task-list-item",[10673,10674],"input",{"checked":266,"disabled":266,"type":10675},"checkbox"," SSO via SAML/OIDC, self-serve configuration",[214,10678,10680,10682],{"className":10679},[10671],[10673,10681],{"checked":266,"disabled":266,"type":10675}," Role-based access control with admin, member, viewer roles",[214,10684,10686,10688],{"className":10685},[10671],[10673,10687],{"checked":266,"disabled":266,"type":10675}," Audit logs with 90-day minimum retention and export",[214,10690,10692,10694],{"className":10691},[10671],[10673,10693],{"checked":266,"disabled":266,"type":10675}," Data export (all organization data, CSV/JSON)",[214,10696,10698,10700],{"className":10697},[10671],[10673,10699],{"checked":266,"disabled":266,"type":10675}," Admin panel for organization management",[214,10702,10704,10706],{"className":10703},[10671],[10673,10705],{"checked":266,"disabled":266,"type":10675}," Customer-managed API keys with scoped permissions",[214,10708,10710,10712],{"className":10709},[10671],[10673,10711],{"checked":266,"disabled":266,"type":10675}," Uptime SLA commitment and status page",[214,10714,10716,10718],{"className":10715},[10671],[10673,10717],{"checked":266,"disabled":266,"type":10675}," SOC 2 Type II or equivalent (or a credible timeline)",[214,10720,10722,10724],{"className":10721},[10671],[10673,10723],{"checked":266,"disabled":266,"type":10675}," Data processing agreement (DPA) template for GDPR",[20,10726,10727],{},"Missing items in this list will come up in enterprise security questionnaires. Better to build them than to lose deals because of them.",[30,10729],{},[20,10731,10732,10733,778],{},"Building for enterprise is an architectural commitment that needs to start early. If you're at the stage where you're beginning to target B2B contracts and want to assess your technical readiness, book a call at ",[197,10734,3424],{"href":199,"rel":10735},[201],[30,10737],{},[15,10739,209],{"id":208},[211,10741,10742,10748,10754,10758],{},[214,10743,10744],{},[197,10745,10747],{"href":10746},"/blog/custom-crm-development","Custom CRM Development: When Building Beats Buying Salesforce",[214,10749,10750],{},[197,10751,10753],{"href":10752},"/blog/saas-development-guide","SaaS Development Guide: From Idea to Paying Customers",[214,10755,10756],{},[197,10757,1349],{"href":2160},[214,10759,10760],{},[197,10761,10763],{"href":10762},"/blog/custom-erp-development-guide","Custom ERP Development: What It Actually Takes",[1309,10765,8560],{},{"title":239,"searchDepth":240,"depth":240,"links":10767},[10768,10769,10770,10771,10772,10773,10774,10775,10776,10777],{"id":10384,"depth":243,"text":10385},{"id":10399,"depth":243,"text":10400},{"id":10428,"depth":243,"text":10429},{"id":10473,"depth":243,"text":10474},{"id":10515,"depth":243,"text":10516},{"id":10530,"depth":243,"text":10531},{"id":10584,"depth":243,"text":10585},{"id":10599,"depth":243,"text":10600},{"id":10659,"depth":243,"text":10660},{"id":208,"depth":243,"text":209},"B2B SaaS has specific technical requirements that consumer SaaS doesn't. Multi-tenancy, SSO, audit logs, role permissions — here's what you actually need to build.",[10780,10781],"B2B SaaS development","enterprise SaaS",{},"/blog/b2b-saas-development",{"title":10378,"description":10778},"blog/b2b-saas-development",[10787,1345,700],"B2B SaaS","68GhU_maWqvu-OmIvZyrGJFDAc_1I7G5WdJAsDeyI-U",{"id":10790,"title":10791,"author":10792,"body":10793,"category":2154,"date":257,"description":13427,"extension":259,"featured":260,"image":261,"keywords":13428,"meta":13431,"navigation":266,"path":13432,"readTime":921,"seo":13433,"stem":13434,"tags":13435,"__hash__":13438},"blog/blog/background-jobs-nodejs.md","Background Jobs in Node.js: Queues, Workers, and Failure Recovery",{"name":9,"bio":10},{"type":12,"value":10794,"toc":13411},[10795,10798,10801,10805,10815,10832,10835,10839,10842,10863,10867,11138,11142,11397,11401,11680,11684,12233,12237,12240,12394,12398,12401,12627,12631,12634,12774,12778,12781,12943,12946,12950,12953,13094,13097,13101,13104,13229,13235,13239,13242,13253,13256,13374,13377,13379,13385,13387,13389,13409],[20,10796,10797],{},"Every production application eventually needs to do work outside of the request-response cycle. Email sending, PDF generation, image processing, webhook delivery, data imports, report generation — these are all things you should not block a user's request waiting for. Background jobs are how you handle them.",[20,10799,10800],{},"The challenge is not adding a job queue — it is building one that behaves correctly when things go wrong: workers crash, jobs fail, the database is temporarily unavailable, or the queue gets backed up. This article walks through the patterns that handle those situations correctly.",[15,10802,10804],{"id":10803},"why-queues-not-just-settimeout","Why Queues, Not Just setTimeout",[20,10806,10807,10808,8232,10811,10814],{},"The temptation is to offload work with ",[741,10809,10810],{},"setTimeout",[741,10812,10813],{},"setImmediate",". This breaks in several ways:",[211,10816,10817,10820,10823,10826,10829],{},[214,10818,10819],{},"Process restarts lose all in-flight work",[214,10821,10822],{},"No visibility into job status or failures",[214,10824,10825],{},"No retry logic for transient failures",[214,10827,10828],{},"No rate limiting for external API calls",[214,10830,10831],{},"No concurrency control for resource-intensive operations",[20,10833,10834],{},"A proper job queue stores jobs durably, tracks their state, provides retry logic, and gives you visibility into what is happening.",[15,10836,10838],{"id":10837},"bullmq-the-standard-choice","BullMQ: The Standard Choice",[20,10840,10841],{},"BullMQ (backed by Redis) is my default for Node.js job queues. It is mature, TypeScript-first, and handles the edge cases correctly.",[734,10843,10847],{"className":10844,"code":10845,"language":10846,"meta":239,"style":239},"language-bash shiki shiki-themes github-dark","npm install bullmq ioredis\n","bash",[741,10848,10849],{"__ignoreMap":239},[850,10850,10851,10854,10857,10860],{"class":852,"line":853},[850,10852,10853],{"class":1667},"npm",[850,10855,10856],{"class":877}," install",[850,10858,10859],{"class":877}," bullmq",[850,10861,10862],{"class":877}," ioredis\n",[15,10864,10866],{"id":10865},"defining-jobs-with-type-safety","Defining Jobs With Type Safety",[734,10868,10870],{"className":1646,"code":10869,"language":1648,"meta":239,"style":239},"// types/jobs.ts\nexport interface EmailJob {\n to: string\n subject: string\n template: 'welcome' | 'reset-password' | 'invoice'\n data: Record\u003Cstring, unknown>\n}\n\nExport interface PdfJob {\n reportId: string\n userId: string\n format: 'pdf' | 'xlsx'\n}\n\nExport interface ImageProcessingJob {\n imageId: string\n operations: Array\u003C{\n type: 'resize' | 'crop' | 'convert'\n params: Record\u003Cstring, unknown>\n }>\n}\n\nExport type JobData = {\n email: EmailJob\n pdf: PdfJob\n 'image-processing': ImageProcessingJob\n}\n",[741,10871,10872,10877,10889,10898,10907,10927,10948,10952,10956,10967,10976,10984,10999,11003,11007,11018,11027,11039,11058,11077,11082,11086,11090,11104,11114,11124,11134],{"__ignoreMap":239},[850,10873,10874],{"class":852,"line":853},[850,10875,10876],{"class":1427},"// types/jobs.ts\n",[850,10878,10879,10881,10884,10887],{"class":852,"line":243},[850,10880,5583],{"class":1660},[850,10882,10883],{"class":1660}," interface",[850,10885,10886],{"class":1667}," EmailJob",[850,10888,1849],{"class":856},[850,10890,10891,10894,10896],{"class":852,"line":240},[850,10892,10893],{"class":1676}," to",[850,10895,1680],{"class":1660},[850,10897,1713],{"class":862},[850,10899,10900,10903,10905],{"class":852,"line":884},[850,10901,10902],{"class":1676}," subject",[850,10904,1680],{"class":1660},[850,10906,1713],{"class":862},[850,10908,10909,10912,10914,10917,10919,10922,10924],{"class":852,"line":897},[850,10910,10911],{"class":1676}," template",[850,10913,1680],{"class":1660},[850,10915,10916],{"class":877}," 'welcome'",[850,10918,1698],{"class":1660},[850,10920,10921],{"class":877}," 'reset-password'",[850,10923,1698],{"class":1660},[850,10925,10926],{"class":877}," 'invoice'\n",[850,10928,10929,10931,10933,10936,10938,10940,10942,10945],{"class":852,"line":910},[850,10930,2028],{"class":1676},[850,10932,1680],{"class":1660},[850,10934,10935],{"class":1667}," Record",[850,10937,1726],{"class":856},[850,10939,6768],{"class":862},[850,10941,797],{"class":856},[850,10943,10944],{"class":862},"unknown",[850,10946,10947],{"class":856},">\n",[850,10949,10950],{"class":852,"line":921},[850,10951,929],{"class":856},[850,10953,10954],{"class":852,"line":268},[850,10955,2650],{"emptyLinePlaceholder":266},[850,10957,10958,10960,10962,10965],{"class":852,"line":1338},[850,10959,3621],{"class":856},[850,10961,1843],{"class":1660},[850,10963,10964],{"class":1667}," PdfJob",[850,10966,1849],{"class":856},[850,10968,10969,10972,10974],{"class":852,"line":1495},[850,10970,10971],{"class":1676}," reportId",[850,10973,1680],{"class":1660},[850,10975,1713],{"class":862},[850,10977,10978,10980,10982],{"class":852,"line":1503},[850,10979,4973],{"class":1676},[850,10981,1680],{"class":1660},[850,10983,1713],{"class":862},[850,10985,10986,10989,10991,10994,10996],{"class":852,"line":1514},[850,10987,10988],{"class":1676}," format",[850,10990,1680],{"class":1660},[850,10992,10993],{"class":877}," 'pdf'",[850,10995,1698],{"class":1660},[850,10997,10998],{"class":877}," 'xlsx'\n",[850,11000,11001],{"class":852,"line":1522},[850,11002,929],{"class":856},[850,11004,11005],{"class":852,"line":1530},[850,11006,2650],{"emptyLinePlaceholder":266},[850,11008,11009,11011,11013,11016],{"class":852,"line":1541},[850,11010,3621],{"class":856},[850,11012,1843],{"class":1660},[850,11014,11015],{"class":1667}," ImageProcessingJob",[850,11017,1849],{"class":856},[850,11019,11020,11023,11025],{"class":852,"line":1548},[850,11021,11022],{"class":1676}," imageId",[850,11024,1680],{"class":1660},[850,11026,1713],{"class":862},[850,11028,11029,11032,11034,11036],{"class":852,"line":1555},[850,11030,11031],{"class":1676}," operations",[850,11033,1680],{"class":1660},[850,11035,1878],{"class":1667},[850,11037,11038],{"class":856},"\u003C{\n",[850,11040,11041,11043,11045,11048,11050,11053,11055],{"class":852,"line":1562},[850,11042,1997],{"class":1676},[850,11044,1680],{"class":1660},[850,11046,11047],{"class":877}," 'resize'",[850,11049,1698],{"class":1660},[850,11051,11052],{"class":877}," 'crop'",[850,11054,1698],{"class":1660},[850,11056,11057],{"class":877}," 'convert'\n",[850,11059,11060,11063,11065,11067,11069,11071,11073,11075],{"class":852,"line":1572},[850,11061,11062],{"class":1676}," params",[850,11064,1680],{"class":1660},[850,11066,10935],{"class":1667},[850,11068,1726],{"class":856},[850,11070,6768],{"class":862},[850,11072,797],{"class":856},[850,11074,10944],{"class":862},[850,11076,10947],{"class":856},[850,11078,11079],{"class":852,"line":1580},[850,11080,11081],{"class":856}," }>\n",[850,11083,11084],{"class":852,"line":1590},[850,11085,929],{"class":856},[850,11087,11088],{"class":852,"line":1597},[850,11089,2650],{"emptyLinePlaceholder":266},[850,11091,11092,11094,11097,11100,11102],{"class":852,"line":1604},[850,11093,3621],{"class":856},[850,11095,11096],{"class":1660},"type",[850,11098,11099],{"class":1667}," JobData",[850,11101,1743],{"class":1660},[850,11103,1849],{"class":856},[850,11105,11106,11109,11111],{"class":852,"line":1611},[850,11107,11108],{"class":1676}," email",[850,11110,1680],{"class":1660},[850,11112,11113],{"class":1667}," EmailJob\n",[850,11115,11116,11119,11121],{"class":852,"line":3798},[850,11117,11118],{"class":1676}," pdf",[850,11120,1680],{"class":1660},[850,11122,11123],{"class":1667}," PdfJob\n",[850,11125,11126,11129,11131],{"class":852,"line":3803},[850,11127,11128],{"class":877}," 'image-processing'",[850,11130,1680],{"class":1660},[850,11132,11133],{"class":1667}," ImageProcessingJob\n",[850,11135,11136],{"class":852,"line":3820},[850,11137,929],{"class":856},[15,11139,11141],{"id":11140},"setting-up-queues","Setting Up Queues",[734,11143,11145],{"className":1646,"code":11144,"language":1648,"meta":239,"style":239},"// queues/index.ts\nimport { Queue } from 'bullmq'\nimport { redis } from '../lib/redis'\nimport type { JobData } from '../types/jobs'\n\nFunction createQueue\u003CK extends keyof JobData>(name: K) {\n return new Queue\u003CJobData[K]>(name, {\n connection: redis,\n defaultJobOptions: {\n attempts: 3,\n backoff: {\n type: 'exponential',\n delay: 2000, // Start at 2s, then 4s, 8s\n },\n removeOnComplete: { count: 100 }, // Keep last 100 completed\n removeOnFail: { count: 500 }, // Keep last 500 failed for debugging\n },\n })\n}\n\nExport const emailQueue = createQueue('email')\nexport const pdfQueue = createQueue('pdf')\nexport const imageQueue = createQueue('image-processing')\n",[741,11146,11147,11152,11164,11176,11190,11194,11223,11245,11250,11255,11264,11269,11279,11292,11296,11308,11320,11324,11328,11332,11336,11357,11377],{"__ignoreMap":239},[850,11148,11149],{"class":852,"line":853},[850,11150,11151],{"class":1427},"// queues/index.ts\n",[850,11153,11154,11156,11159,11161],{"class":852,"line":243},[850,11155,3546],{"class":1660},[850,11157,11158],{"class":856}," { Queue } ",[850,11160,3552],{"class":1660},[850,11162,11163],{"class":877}," 'bullmq'\n",[850,11165,11166,11168,11171,11173],{"class":852,"line":240},[850,11167,3546],{"class":1660},[850,11169,11170],{"class":856}," { redis } ",[850,11172,3552],{"class":1660},[850,11174,11175],{"class":877}," '../lib/redis'\n",[850,11177,11178,11180,11182,11185,11187],{"class":852,"line":884},[850,11179,3546],{"class":1660},[850,11181,1997],{"class":1660},[850,11183,11184],{"class":856}," { JobData } ",[850,11186,3552],{"class":1660},[850,11188,11189],{"class":877}," '../types/jobs'\n",[850,11191,11192],{"class":852,"line":897},[850,11193,2650],{"emptyLinePlaceholder":266},[850,11195,11196,11199,11202,11205,11208,11210,11213,11216,11218,11221],{"class":852,"line":910},[850,11197,11198],{"class":856},"Function createQueue\u003C",[850,11200,11201],{"class":1667},"K",[850,11203,11204],{"class":1660}," extends",[850,11206,11207],{"class":1660}," keyof",[850,11209,11099],{"class":1667},[850,11211,11212],{"class":856},">(",[850,11214,11215],{"class":1676},"name",[850,11217,1680],{"class":1660},[850,11219,11220],{"class":1667}," K",[850,11222,2745],{"class":856},[850,11224,11225,11227,11229,11232,11234,11237,11240,11242],{"class":852,"line":921},[850,11226,1757],{"class":1660},[850,11228,3131],{"class":1660},[850,11230,11231],{"class":1667}," Queue",[850,11233,1726],{"class":856},[850,11235,11236],{"class":1667},"JobData",[850,11238,11239],{"class":856},"[",[850,11241,11201],{"class":1667},[850,11243,11244],{"class":856},"]>(name, {\n",[850,11246,11247],{"class":852,"line":268},[850,11248,11249],{"class":856}," connection: redis,\n",[850,11251,11252],{"class":852,"line":1338},[850,11253,11254],{"class":856}," defaultJobOptions: {\n",[850,11256,11257,11260,11262],{"class":852,"line":1495},[850,11258,11259],{"class":856}," attempts: ",[850,11261,8867],{"class":862},[850,11263,881],{"class":856},[850,11265,11266],{"class":852,"line":1503},[850,11267,11268],{"class":856}," backoff: {\n",[850,11270,11271,11274,11277],{"class":852,"line":1514},[850,11272,11273],{"class":856}," type: ",[850,11275,11276],{"class":877},"'exponential'",[850,11278,881],{"class":856},[850,11280,11281,11284,11287,11289],{"class":852,"line":1522},[850,11282,11283],{"class":856}," delay: ",[850,11285,11286],{"class":862},"2000",[850,11288,797],{"class":856},[850,11290,11291],{"class":1427},"// Start at 2s, then 4s, 8s\n",[850,11293,11294],{"class":852,"line":1530},[850,11295,4720],{"class":856},[850,11297,11298,11301,11303,11305],{"class":852,"line":1541},[850,11299,11300],{"class":856}," removeOnComplete: { count: ",[850,11302,3148],{"class":862},[850,11304,4725],{"class":856},[850,11306,11307],{"class":1427},"// Keep last 100 completed\n",[850,11309,11310,11313,11315,11317],{"class":852,"line":1548},[850,11311,11312],{"class":856}," removeOnFail: { count: ",[850,11314,4927],{"class":862},[850,11316,4725],{"class":856},[850,11318,11319],{"class":1427},"// Keep last 500 failed for debugging\n",[850,11321,11322],{"class":852,"line":1555},[850,11323,4720],{"class":856},[850,11325,11326],{"class":852,"line":1562},[850,11327,2697],{"class":856},[850,11329,11330],{"class":852,"line":1572},[850,11331,929],{"class":856},[850,11333,11334],{"class":852,"line":1580},[850,11335,2650],{"emptyLinePlaceholder":266},[850,11337,11338,11340,11342,11345,11347,11350,11352,11355],{"class":852,"line":1590},[850,11339,3621],{"class":856},[850,11341,3123],{"class":1660},[850,11343,11344],{"class":862}," emailQueue",[850,11346,1743],{"class":1660},[850,11348,11349],{"class":1667}," createQueue",[850,11351,1766],{"class":856},[850,11353,11354],{"class":877},"'email'",[850,11356,1772],{"class":856},[850,11358,11359,11361,11363,11366,11368,11370,11372,11375],{"class":852,"line":1597},[850,11360,5583],{"class":1660},[850,11362,1737],{"class":1660},[850,11364,11365],{"class":862}," pdfQueue",[850,11367,1743],{"class":1660},[850,11369,11349],{"class":1667},[850,11371,1766],{"class":856},[850,11373,11374],{"class":877},"'pdf'",[850,11376,1772],{"class":856},[850,11378,11379,11381,11383,11386,11388,11390,11392,11395],{"class":852,"line":1604},[850,11380,5583],{"class":1660},[850,11382,1737],{"class":1660},[850,11384,11385],{"class":862}," imageQueue",[850,11387,1743],{"class":1660},[850,11389,11349],{"class":1667},[850,11391,1766],{"class":856},[850,11393,11394],{"class":877},"'image-processing'",[850,11396,1772],{"class":856},[15,11398,11400],{"id":11399},"adding-jobs","Adding Jobs",[734,11402,11404],{"className":1646,"code":11403,"language":1648,"meta":239,"style":239},"// In your API handlers\nawait emailQueue.add('send-welcome', {\n to: user.email,\n subject: 'Welcome to the platform',\n template: 'welcome',\n data: { name: user.name, activationUrl: `https://app.com/activate/${token}` },\n})\n\n// Priority jobs (lower number = higher priority)\nawait emailQueue.add(\n 'send-password-reset',\n {\n to: user.email,\n subject: 'Reset your password',\n template: 'reset-password',\n data: { resetUrl: `https://app.com/reset/${token}` },\n },\n { priority: 1 } // Process before normal priority jobs\n)\n\n// Delayed jobs\nawait emailQueue.add(\n 'send-trial-expiry-warning',\n { to: user.email, template: 'trial-expiry', data: {} },\n { delay: 7 * 24 * 60 * 60 * 1000 } // 7 days from now\n)\n\n// Scheduled recurring jobs\nawait emailQueue.add(\n 'weekly-digest',\n { to: user.email, template: 'weekly-digest', data: {} },\n { repeat: { cron: '0 9 * * 1' } } // Every Monday at 9am\n)\n",[741,11405,11406,11411,11429,11434,11444,11454,11468,11472,11476,11481,11491,11498,11502,11506,11515,11524,11538,11542,11554,11558,11562,11567,11577,11584,11595,11623,11627,11631,11636,11646,11653,11662,11676],{"__ignoreMap":239},[850,11407,11408],{"class":852,"line":853},[850,11409,11410],{"class":1427},"// In your API handlers\n",[850,11412,11413,11416,11419,11422,11424,11427],{"class":852,"line":243},[850,11414,11415],{"class":1660},"await",[850,11417,11418],{"class":856}," emailQueue.",[850,11420,11421],{"class":1667},"add",[850,11423,1766],{"class":856},[850,11425,11426],{"class":877},"'send-welcome'",[850,11428,5281],{"class":856},[850,11430,11431],{"class":852,"line":240},[850,11432,11433],{"class":856}," to: user.email,\n",[850,11435,11436,11439,11442],{"class":852,"line":884},[850,11437,11438],{"class":856}," subject: ",[850,11440,11441],{"class":877},"'Welcome to the platform'",[850,11443,881],{"class":856},[850,11445,11446,11449,11452],{"class":852,"line":897},[850,11447,11448],{"class":856}," template: ",[850,11450,11451],{"class":877},"'welcome'",[850,11453,881],{"class":856},[850,11455,11456,11459,11462,11464,11466],{"class":852,"line":910},[850,11457,11458],{"class":856}," data: { name: user.name, activationUrl: ",[850,11460,11461],{"class":877},"`https://app.com/activate/${",[850,11463,9744],{"class":856},[850,11465,3912],{"class":877},[850,11467,4720],{"class":856},[850,11469,11470],{"class":852,"line":921},[850,11471,2702],{"class":856},[850,11473,11474],{"class":852,"line":268},[850,11475,2650],{"emptyLinePlaceholder":266},[850,11477,11478],{"class":852,"line":1338},[850,11479,11480],{"class":1427},"// Priority jobs (lower number = higher priority)\n",[850,11482,11483,11485,11487,11489],{"class":852,"line":1495},[850,11484,11415],{"class":1660},[850,11486,11418],{"class":856},[850,11488,11421],{"class":1667},[850,11490,1671],{"class":856},[850,11492,11493,11496],{"class":852,"line":1503},[850,11494,11495],{"class":877}," 'send-password-reset'",[850,11497,881],{"class":856},[850,11499,11500],{"class":852,"line":1514},[850,11501,1849],{"class":856},[850,11503,11504],{"class":852,"line":1522},[850,11505,11433],{"class":856},[850,11507,11508,11510,11513],{"class":852,"line":1530},[850,11509,11438],{"class":856},[850,11511,11512],{"class":877},"'Reset your password'",[850,11514,881],{"class":856},[850,11516,11517,11519,11522],{"class":852,"line":1541},[850,11518,11448],{"class":856},[850,11520,11521],{"class":877},"'reset-password'",[850,11523,881],{"class":856},[850,11525,11526,11529,11532,11534,11536],{"class":852,"line":1548},[850,11527,11528],{"class":856}," data: { resetUrl: ",[850,11530,11531],{"class":877},"`https://app.com/reset/${",[850,11533,9744],{"class":856},[850,11535,3912],{"class":877},[850,11537,4720],{"class":856},[850,11539,11540],{"class":852,"line":1555},[850,11541,4720],{"class":856},[850,11543,11544,11547,11549,11551],{"class":852,"line":1562},[850,11545,11546],{"class":856}," { priority: ",[850,11548,3976],{"class":862},[850,11550,3736],{"class":856},[850,11552,11553],{"class":1427},"// Process before normal priority jobs\n",[850,11555,11556],{"class":852,"line":1572},[850,11557,1772],{"class":856},[850,11559,11560],{"class":852,"line":1580},[850,11561,2650],{"emptyLinePlaceholder":266},[850,11563,11564],{"class":852,"line":1590},[850,11565,11566],{"class":1427},"// Delayed jobs\n",[850,11568,11569,11571,11573,11575],{"class":852,"line":1597},[850,11570,11415],{"class":1660},[850,11572,11418],{"class":856},[850,11574,11421],{"class":1667},[850,11576,1671],{"class":856},[850,11578,11579,11582],{"class":852,"line":1604},[850,11580,11581],{"class":877}," 'send-trial-expiry-warning'",[850,11583,881],{"class":856},[850,11585,11586,11589,11592],{"class":852,"line":1611},[850,11587,11588],{"class":856}," { to: user.email, template: ",[850,11590,11591],{"class":877},"'trial-expiry'",[850,11593,11594],{"class":856},", data: {} },\n",[850,11596,11597,11600,11602,11604,11606,11608,11610,11612,11614,11616,11618,11620],{"class":852,"line":3798},[850,11598,11599],{"class":856}," { delay: ",[850,11601,9329],{"class":862},[850,11603,4806],{"class":1660},[850,11605,9334],{"class":862},[850,11607,4806],{"class":1660},[850,11609,4809],{"class":862},[850,11611,4806],{"class":1660},[850,11613,4809],{"class":862},[850,11615,4806],{"class":1660},[850,11617,4232],{"class":862},[850,11619,3736],{"class":856},[850,11621,11622],{"class":1427},"// 7 days from now\n",[850,11624,11625],{"class":852,"line":3803},[850,11626,1772],{"class":856},[850,11628,11629],{"class":852,"line":3820},[850,11630,2650],{"emptyLinePlaceholder":266},[850,11632,11633],{"class":852,"line":3825},[850,11634,11635],{"class":1427},"// Scheduled recurring jobs\n",[850,11637,11638,11640,11642,11644],{"class":852,"line":3831},[850,11639,11415],{"class":1660},[850,11641,11418],{"class":856},[850,11643,11421],{"class":1667},[850,11645,1671],{"class":856},[850,11647,11648,11651],{"class":852,"line":3849},[850,11649,11650],{"class":877}," 'weekly-digest'",[850,11652,881],{"class":856},[850,11654,11655,11657,11660],{"class":852,"line":3854},[850,11656,11588],{"class":856},[850,11658,11659],{"class":877},"'weekly-digest'",[850,11661,11594],{"class":856},[850,11663,11664,11667,11670,11673],{"class":852,"line":3860},[850,11665,11666],{"class":856}," { repeat: { cron: ",[850,11668,11669],{"class":877},"'0 9 * * 1'",[850,11671,11672],{"class":856}," } } ",[850,11674,11675],{"class":1427},"// Every Monday at 9am\n",[850,11677,11678],{"class":852,"line":3871},[850,11679,1772],{"class":856},[15,11681,11683],{"id":11682},"worker-implementation","Worker Implementation",[734,11685,11687],{"className":1646,"code":11686,"language":1648,"meta":239,"style":239},"// workers/email.ts\nimport { Worker, UnrecoverableError } from 'bullmq'\nimport { redis } from '../lib/redis'\nimport type { EmailJob } from '../types/jobs'\n\nConst emailWorker = new Worker\u003CEmailJob>(\n 'email',\n async (job) => {\n const { to, subject, template, data } = job.data\n\n job.log(`Sending ${template} email to ${to}`)\n await job.updateProgress(10)\n\n // Render the email template\n const html = await renderTemplate(template, data)\n await job.updateProgress(40)\n\n // Send via your email provider\n await sendEmail({ to, subject, html })\n await job.updateProgress(100)\n\n return { sentAt: new Date().toISOString() }\n },\n {\n connection: redis,\n concurrency: 10, // Process 10 emails simultaneously\n limiter: {\n max: 100, // Max 100 jobs per interval\n duration: 1000, // Per second\n },\n }\n)\n\n// Handle worker events\nemailWorker.on('completed', (job) => {\n console.log(`Job ${job.id} completed`)\n})\n\nEmailWorker.on('failed', (job, err) => {\n console.error(`Job ${job?.id} failed:`, err.message)\n\n // Alert on final failure (all retries exhausted)\n if ((job?.attemptsMade ?? 0) >= (job?.opts.attempts ?? 1)) {\n console.error(`Job permanently failed after ${job?.attemptsMade} attempts`)\n // Send alert to your monitoring system\n }\n})\n\nEmailWorker.on('error', (err) => {\n console.error('Worker error:', err)\n})\n",[741,11688,11689,11694,11705,11715,11728,11732,11752,11759,11774,11805,11809,11833,11848,11852,11857,11874,11889,11893,11898,11908,11922,11926,11944,11948,11952,11956,11968,11973,11984,11996,12000,12004,12008,12012,12017,12040,12062,12066,12070,12097,12119,12123,12128,12155,12178,12183,12187,12191,12195,12216,12229],{"__ignoreMap":239},[850,11690,11691],{"class":852,"line":853},[850,11692,11693],{"class":1427},"// workers/email.ts\n",[850,11695,11696,11698,11701,11703],{"class":852,"line":243},[850,11697,3546],{"class":1660},[850,11699,11700],{"class":856}," { Worker, UnrecoverableError } ",[850,11702,3552],{"class":1660},[850,11704,11163],{"class":877},[850,11706,11707,11709,11711,11713],{"class":852,"line":240},[850,11708,3546],{"class":1660},[850,11710,11170],{"class":856},[850,11712,3552],{"class":1660},[850,11714,11175],{"class":877},[850,11716,11717,11719,11721,11724,11726],{"class":852,"line":884},[850,11718,3546],{"class":1660},[850,11720,1997],{"class":1660},[850,11722,11723],{"class":856}," { EmailJob } ",[850,11725,3552],{"class":1660},[850,11727,11189],{"class":877},[850,11729,11730],{"class":852,"line":897},[850,11731,2650],{"emptyLinePlaceholder":266},[850,11733,11734,11737,11739,11741,11744,11746,11749],{"class":852,"line":910},[850,11735,11736],{"class":856},"Const emailWorker ",[850,11738,3251],{"class":1660},[850,11740,3131],{"class":1660},[850,11742,11743],{"class":1667}," Worker",[850,11745,1726],{"class":856},[850,11747,11748],{"class":1667},"EmailJob",[850,11750,11751],{"class":856},">(\n",[850,11753,11754,11757],{"class":852,"line":921},[850,11755,11756],{"class":877}," 'email'",[850,11758,881],{"class":856},[850,11760,11761,11763,11765,11768,11770,11772],{"class":852,"line":268},[850,11762,5586],{"class":1660},[850,11764,1123],{"class":856},[850,11766,11767],{"class":1676},"job",[850,11769,2591],{"class":856},[850,11771,2594],{"class":1660},[850,11773,1849],{"class":856},[850,11775,11776,11778,11780,11783,11785,11788,11790,11793,11795,11798,11800,11802],{"class":852,"line":1338},[850,11777,1737],{"class":1660},[850,11779,3715],{"class":856},[850,11781,11782],{"class":862},"to",[850,11784,797],{"class":856},[850,11786,11787],{"class":862},"subject",[850,11789,797],{"class":856},[850,11791,11792],{"class":862},"template",[850,11794,797],{"class":856},[850,11796,11797],{"class":862},"data",[850,11799,3736],{"class":856},[850,11801,3251],{"class":1660},[850,11803,11804],{"class":856}," job.data\n",[850,11806,11807],{"class":852,"line":1495},[850,11808,2650],{"emptyLinePlaceholder":266},[850,11810,11811,11814,11817,11819,11822,11824,11827,11829,11831],{"class":852,"line":1503},[850,11812,11813],{"class":856}," job.",[850,11815,11816],{"class":1667},"log",[850,11818,1766],{"class":856},[850,11820,11821],{"class":877},"`Sending ${",[850,11823,11792],{"class":856},[850,11825,11826],{"class":877},"} email to ${",[850,11828,11782],{"class":856},[850,11830,3912],{"class":877},[850,11832,1772],{"class":856},[850,11834,11835,11837,11839,11842,11844,11846],{"class":852,"line":1514},[850,11836,1746],{"class":1660},[850,11838,11813],{"class":856},[850,11840,11841],{"class":1667},"updateProgress",[850,11843,1766],{"class":856},[850,11845,4863],{"class":862},[850,11847,1772],{"class":856},[850,11849,11850],{"class":852,"line":1522},[850,11851,2650],{"emptyLinePlaceholder":266},[850,11853,11854],{"class":852,"line":1530},[850,11855,11856],{"class":1427}," // Render the email template\n",[850,11858,11859,11861,11864,11866,11868,11871],{"class":852,"line":1541},[850,11860,1737],{"class":1660},[850,11862,11863],{"class":862}," html",[850,11865,1743],{"class":1660},[850,11867,1746],{"class":1660},[850,11869,11870],{"class":1667}," renderTemplate",[850,11872,11873],{"class":856},"(template, data)\n",[850,11875,11876,11878,11880,11882,11884,11887],{"class":852,"line":1548},[850,11877,1746],{"class":1660},[850,11879,11813],{"class":856},[850,11881,11841],{"class":1667},[850,11883,1766],{"class":856},[850,11885,11886],{"class":862},"40",[850,11888,1772],{"class":856},[850,11890,11891],{"class":852,"line":1555},[850,11892,2650],{"emptyLinePlaceholder":266},[850,11894,11895],{"class":852,"line":1562},[850,11896,11897],{"class":1427}," // Send via your email provider\n",[850,11899,11900,11902,11905],{"class":852,"line":1572},[850,11901,1746],{"class":1660},[850,11903,11904],{"class":1667}," sendEmail",[850,11906,11907],{"class":856},"({ to, subject, html })\n",[850,11909,11910,11912,11914,11916,11918,11920],{"class":852,"line":1580},[850,11911,1746],{"class":1660},[850,11913,11813],{"class":856},[850,11915,11841],{"class":1667},[850,11917,1766],{"class":856},[850,11919,3148],{"class":862},[850,11921,1772],{"class":856},[850,11923,11924],{"class":852,"line":1590},[850,11925,2650],{"emptyLinePlaceholder":266},[850,11927,11928,11930,11933,11935,11937,11939,11942],{"class":852,"line":1597},[850,11929,1757],{"class":1660},[850,11931,11932],{"class":856}," { sentAt: ",[850,11934,3369],{"class":1660},[850,11936,4150],{"class":1667},[850,11938,6771],{"class":856},[850,11940,11941],{"class":1667},"toISOString",[850,11943,5691],{"class":856},[850,11945,11946],{"class":852,"line":1604},[850,11947,4720],{"class":856},[850,11949,11950],{"class":852,"line":1611},[850,11951,1849],{"class":856},[850,11953,11954],{"class":852,"line":3798},[850,11955,11249],{"class":856},[850,11957,11958,11961,11963,11965],{"class":852,"line":3803},[850,11959,11960],{"class":856}," concurrency: ",[850,11962,4863],{"class":862},[850,11964,797],{"class":856},[850,11966,11967],{"class":1427},"// Process 10 emails simultaneously\n",[850,11969,11970],{"class":852,"line":3820},[850,11971,11972],{"class":856}," limiter: {\n",[850,11974,11975,11977,11979,11981],{"class":852,"line":3825},[850,11976,6577],{"class":856},[850,11978,3148],{"class":862},[850,11980,797],{"class":856},[850,11982,11983],{"class":1427},"// Max 100 jobs per interval\n",[850,11985,11986,11989,11991,11993],{"class":852,"line":3831},[850,11987,11988],{"class":856}," duration: ",[850,11990,4793],{"class":862},[850,11992,797],{"class":856},[850,11994,11995],{"class":1427},"// Per second\n",[850,11997,11998],{"class":852,"line":3849},[850,11999,4720],{"class":856},[850,12001,12002],{"class":852,"line":3854},[850,12003,924],{"class":856},[850,12005,12006],{"class":852,"line":3860},[850,12007,1772],{"class":856},[850,12009,12010],{"class":852,"line":3871},[850,12011,2650],{"emptyLinePlaceholder":266},[850,12013,12014],{"class":852,"line":3876},[850,12015,12016],{"class":1427},"// Handle worker events\n",[850,12018,12019,12022,12024,12026,12029,12032,12034,12036,12038],{"class":852,"line":3882},[850,12020,12021],{"class":856},"emailWorker.",[850,12023,7577],{"class":1667},[850,12025,1766],{"class":856},[850,12027,12028],{"class":877},"'completed'",[850,12030,12031],{"class":856},", (",[850,12033,11767],{"class":1676},[850,12035,2591],{"class":856},[850,12037,2594],{"class":1660},[850,12039,1849],{"class":856},[850,12041,12042,12044,12046,12048,12051,12053,12055,12057,12060],{"class":852,"line":3917},[850,12043,5662],{"class":856},[850,12045,11816],{"class":1667},[850,12047,1766],{"class":856},[850,12049,12050],{"class":877},"`Job ${",[850,12052,11767],{"class":856},[850,12054,778],{"class":877},[850,12056,6187],{"class":856},[850,12058,12059],{"class":877},"} completed`",[850,12061,1772],{"class":856},[850,12063,12064],{"class":852,"line":3922},[850,12065,2702],{"class":856},[850,12067,12068],{"class":852,"line":3928},[850,12069,2650],{"emptyLinePlaceholder":266},[850,12071,12072,12075,12077,12079,12082,12084,12086,12088,12091,12093,12095],{"class":852,"line":3939},[850,12073,12074],{"class":856},"EmailWorker.",[850,12076,7577],{"class":1667},[850,12078,1766],{"class":856},[850,12080,12081],{"class":877},"'failed'",[850,12083,12031],{"class":856},[850,12085,11767],{"class":1676},[850,12087,797],{"class":856},[850,12089,12090],{"class":1676},"err",[850,12092,2591],{"class":856},[850,12094,2594],{"class":1660},[850,12096,1849],{"class":856},[850,12098,12099,12101,12103,12105,12107,12109,12111,12113,12116],{"class":852,"line":3944},[850,12100,5662],{"class":856},[850,12102,5665],{"class":1667},[850,12104,1766],{"class":856},[850,12106,12050],{"class":877},[850,12108,11767],{"class":856},[850,12110,7275],{"class":877},[850,12112,6187],{"class":856},[850,12114,12115],{"class":877},"} failed:`",[850,12117,12118],{"class":856},", err.message)\n",[850,12120,12121],{"class":852,"line":3963},[850,12122,2650],{"emptyLinePlaceholder":266},[850,12124,12125],{"class":852,"line":4000},[850,12126,12127],{"class":1427}," // Alert on final failure (all retries exhausted)\n",[850,12129,12130,12132,12135,12137,12140,12142,12145,12148,12150,12152],{"class":852,"line":4005},[850,12131,2947],{"class":1660},[850,12133,12134],{"class":856}," ((job?.attemptsMade ",[850,12136,3994],{"class":1660},[850,12138,12139],{"class":862}," 0",[850,12141,2591],{"class":856},[850,12143,12144],{"class":1660},">=",[850,12146,12147],{"class":856}," (job?.opts.attempts ",[850,12149,3994],{"class":1660},[850,12151,4051],{"class":862},[850,12153,12154],{"class":856},")) {\n",[850,12156,12157,12159,12161,12163,12166,12168,12170,12173,12176],{"class":852,"line":4022},[850,12158,5662],{"class":856},[850,12160,5665],{"class":1667},[850,12162,1766],{"class":856},[850,12164,12165],{"class":877},"`Job permanently failed after ${",[850,12167,11767],{"class":856},[850,12169,7275],{"class":877},[850,12171,12172],{"class":856},"attemptsMade",[850,12174,12175],{"class":877},"} attempts`",[850,12177,1772],{"class":856},[850,12179,12180],{"class":852,"line":4056},[850,12181,12182],{"class":1427}," // Send alert to your monitoring system\n",[850,12184,12185],{"class":852,"line":4061},[850,12186,924],{"class":856},[850,12188,12189],{"class":852,"line":4074},[850,12190,2702],{"class":856},[850,12192,12193],{"class":852,"line":4080},[850,12194,2650],{"emptyLinePlaceholder":266},[850,12196,12197,12199,12201,12203,12206,12208,12210,12212,12214],{"class":852,"line":4112},[850,12198,12074],{"class":856},[850,12200,7577],{"class":1667},[850,12202,1766],{"class":856},[850,12204,12205],{"class":877},"'error'",[850,12207,12031],{"class":856},[850,12209,12090],{"class":1676},[850,12211,2591],{"class":856},[850,12213,2594],{"class":1660},[850,12215,1849],{"class":856},[850,12217,12218,12220,12222,12224,12227],{"class":852,"line":4139},[850,12219,5662],{"class":856},[850,12221,5665],{"class":1667},[850,12223,1766],{"class":856},[850,12225,12226],{"class":877},"'Worker error:'",[850,12228,5740],{"class":856},[850,12230,12231],{"class":852,"line":4162},[850,12232,2702],{"class":856},[15,12234,12236],{"id":12235},"non-retryable-errors","Non-Retryable Errors",[20,12238,12239],{},"Some failures should not be retried. If an email address is permanently invalid or a user does not exist, retrying wastes resources and clutters your failed job logs.",[734,12241,12243],{"className":1646,"code":12242,"language":1648,"meta":239,"style":239},"import { UnrecoverableError } from 'bullmq'\n\nAsync (job) => {\n const user = await db.user.findUnique({ where: { id: job.data.userId } })\n\n if (!user) {\n // Throw UnrecoverableError to skip retries\n throw new UnrecoverableError(`User ${job.data.userId} not found`)\n }\n\n if (user.emailBounced) {\n throw new UnrecoverableError(`Email bounced for user ${user.email}`)\n }\n\n // ... Proceed with sending\n}\n",[741,12244,12245,12256,12260,12272,12289,12293,12304,12309,12339,12343,12347,12354,12377,12381,12385,12390],{"__ignoreMap":239},[850,12246,12247,12249,12252,12254],{"class":852,"line":853},[850,12248,3546],{"class":1660},[850,12250,12251],{"class":856}," { UnrecoverableError } ",[850,12253,3552],{"class":1660},[850,12255,11163],{"class":877},[850,12257,12258],{"class":852,"line":243},[850,12259,2650],{"emptyLinePlaceholder":266},[850,12261,12262,12265,12268,12270],{"class":852,"line":240},[850,12263,12264],{"class":1667},"Async",[850,12266,12267],{"class":856}," (job) ",[850,12269,2594],{"class":1660},[850,12271,1849],{"class":856},[850,12273,12274,12276,12278,12280,12282,12284,12286],{"class":852,"line":884},[850,12275,1737],{"class":1660},[850,12277,3196],{"class":862},[850,12279,1743],{"class":1660},[850,12281,1746],{"class":1660},[850,12283,7159],{"class":856},[850,12285,2777],{"class":1667},[850,12287,12288],{"class":856},"({ where: { id: job.data.userId } })\n",[850,12290,12291],{"class":852,"line":897},[850,12292,2650],{"emptyLinePlaceholder":266},[850,12294,12295,12297,12299,12301],{"class":852,"line":910},[850,12296,2947],{"class":1660},[850,12298,1123],{"class":856},[850,12300,4068],{"class":1660},[850,12302,12303],{"class":856},"user) {\n",[850,12305,12306],{"class":852,"line":921},[850,12307,12308],{"class":1427}," // Throw UnrecoverableError to skip retries\n",[850,12310,12311,12314,12316,12319,12321,12324,12326,12328,12330,12332,12334,12337],{"class":852,"line":268},[850,12312,12313],{"class":1660}," throw",[850,12315,3131],{"class":1660},[850,12317,12318],{"class":1667}," UnrecoverableError",[850,12320,1766],{"class":856},[850,12322,12323],{"class":877},"`User ${",[850,12325,11767],{"class":856},[850,12327,778],{"class":877},[850,12329,11797],{"class":856},[850,12331,778],{"class":877},[850,12333,6240],{"class":856},[850,12335,12336],{"class":877},"} not found`",[850,12338,1772],{"class":856},[850,12340,12341],{"class":852,"line":1338},[850,12342,924],{"class":856},[850,12344,12345],{"class":852,"line":1495},[850,12346,2650],{"emptyLinePlaceholder":266},[850,12348,12349,12351],{"class":852,"line":1503},[850,12350,2947],{"class":1660},[850,12352,12353],{"class":856}," (user.emailBounced) {\n",[850,12355,12356,12358,12360,12362,12364,12367,12369,12371,12373,12375],{"class":852,"line":1514},[850,12357,12313],{"class":1660},[850,12359,3131],{"class":1660},[850,12361,12318],{"class":1667},[850,12363,1766],{"class":856},[850,12365,12366],{"class":877},"`Email bounced for user ${",[850,12368,3240],{"class":856},[850,12370,778],{"class":877},[850,12372,9680],{"class":856},[850,12374,3912],{"class":877},[850,12376,1772],{"class":856},[850,12378,12379],{"class":852,"line":1522},[850,12380,924],{"class":856},[850,12382,12383],{"class":852,"line":1530},[850,12384,2650],{"emptyLinePlaceholder":266},[850,12386,12387],{"class":852,"line":1541},[850,12388,12389],{"class":1427}," // ... Proceed with sending\n",[850,12391,12392],{"class":852,"line":1548},[850,12393,929],{"class":856},[15,12395,12397],{"id":12396},"job-progress-and-logging","Job Progress and Logging",[20,12399,12400],{},"Progress tracking gives you visibility into long-running jobs:",[734,12402,12404],{"className":1646,"code":12403,"language":1648,"meta":239,"style":239},"async (job) => {\n const rows = await db.select().from(users).where(eq(users.status, 'active'))\n const total = rows.length\n\n for (let i = 0; i \u003C rows.length; i++) {\n await processUser(rows[i])\n\n // Update progress\n await job.updateProgress(Math.round((i / total) * 100))\n\n // Log to job's log (visible in Bull Board)\n if (i % 100 === 0) {\n job.log(`Processed ${i}/${total} users`)\n }\n }\n}\n",[741,12405,12406,12420,12461,12476,12480,12515,12525,12529,12534,12562,12566,12571,12590,12615,12619,12623],{"__ignoreMap":239},[850,12407,12408,12410,12412,12414,12416,12418],{"class":852,"line":853},[850,12409,1661],{"class":1660},[850,12411,1123],{"class":856},[850,12413,11767],{"class":1676},[850,12415,2591],{"class":856},[850,12417,2594],{"class":1660},[850,12419,1849],{"class":856},[850,12421,12422,12424,12427,12429,12431,12434,12437,12439,12441,12444,12447,12449,12452,12455,12458],{"class":852,"line":243},[850,12423,1737],{"class":1660},[850,12425,12426],{"class":862}," rows",[850,12428,1743],{"class":1660},[850,12430,1746],{"class":1660},[850,12432,12433],{"class":856}," db.",[850,12435,12436],{"class":1667},"select",[850,12438,6771],{"class":856},[850,12440,3552],{"class":1667},[850,12442,12443],{"class":856},"(users).",[850,12445,12446],{"class":1667},"where",[850,12448,1766],{"class":856},[850,12450,12451],{"class":1667},"eq",[850,12453,12454],{"class":856},"(users.status, ",[850,12456,12457],{"class":877},"'active'",[850,12459,12460],{"class":856},"))\n",[850,12462,12463,12465,12468,12470,12473],{"class":852,"line":240},[850,12464,1737],{"class":1660},[850,12466,12467],{"class":862}," total",[850,12469,1743],{"class":1660},[850,12471,12472],{"class":856}," rows.",[850,12474,12475],{"class":862},"length\n",[850,12477,12478],{"class":852,"line":884},[850,12479,2650],{"emptyLinePlaceholder":266},[850,12481,12482,12485,12487,12490,12493,12495,12497,12500,12502,12504,12507,12510,12513],{"class":852,"line":897},[850,12483,12484],{"class":1660}," for",[850,12486,1123],{"class":856},[850,12488,12489],{"class":1660},"let",[850,12491,12492],{"class":856}," i ",[850,12494,3251],{"class":1660},[850,12496,12139],{"class":862},[850,12498,12499],{"class":856},"; i ",[850,12501,1726],{"class":1660},[850,12503,12472],{"class":856},[850,12505,12506],{"class":862},"length",[850,12508,12509],{"class":856},"; i",[850,12511,12512],{"class":1660},"++",[850,12514,2745],{"class":856},[850,12516,12517,12519,12522],{"class":852,"line":910},[850,12518,1746],{"class":1660},[850,12520,12521],{"class":1667}," processUser",[850,12523,12524],{"class":856},"(rows[i])\n",[850,12526,12527],{"class":852,"line":921},[850,12528,2650],{"emptyLinePlaceholder":266},[850,12530,12531],{"class":852,"line":268},[850,12532,12533],{"class":1427}," // Update progress\n",[850,12535,12536,12538,12540,12542,12544,12547,12550,12552,12555,12557,12560],{"class":852,"line":1338},[850,12537,1746],{"class":1660},[850,12539,11813],{"class":856},[850,12541,11841],{"class":1667},[850,12543,4595],{"class":856},[850,12545,12546],{"class":1667},"round",[850,12548,12549],{"class":856},"((i ",[850,12551,4229],{"class":1660},[850,12553,12554],{"class":856}," total) ",[850,12556,7376],{"class":1660},[850,12558,12559],{"class":862}," 100",[850,12561,12460],{"class":856},[850,12563,12564],{"class":852,"line":1495},[850,12565,2650],{"emptyLinePlaceholder":266},[850,12567,12568],{"class":852,"line":1503},[850,12569,12570],{"class":1427}," // Log to job's log (visible in Bull Board)\n",[850,12572,12573,12575,12578,12581,12583,12586,12588],{"class":852,"line":1514},[850,12574,2947],{"class":1660},[850,12576,12577],{"class":856}," (i ",[850,12579,12580],{"class":1660},"%",[850,12582,12559],{"class":862},[850,12584,12585],{"class":1660}," ===",[850,12587,12139],{"class":862},[850,12589,2745],{"class":856},[850,12591,12592,12594,12596,12598,12601,12604,12607,12610,12613],{"class":852,"line":1522},[850,12593,11813],{"class":856},[850,12595,11816],{"class":1667},[850,12597,1766],{"class":856},[850,12599,12600],{"class":877},"`Processed ${",[850,12602,12603],{"class":856},"i",[850,12605,12606],{"class":877},"}/${",[850,12608,12609],{"class":856},"total",[850,12611,12612],{"class":877},"} users`",[850,12614,1772],{"class":856},[850,12616,12617],{"class":852,"line":1530},[850,12618,924],{"class":856},[850,12620,12621],{"class":852,"line":1541},[850,12622,924],{"class":856},[850,12624,12625],{"class":852,"line":1548},[850,12626,929],{"class":856},[15,12628,12630],{"id":12629},"priority-queues","Priority Queues",[20,12632,12633],{},"For applications with multiple job types competing for worker resources, use priority:",[734,12635,12637],{"className":1646,"code":12636,"language":1648,"meta":239,"style":239},"const reportQueue = new Queue('reports', {\n connection: redis,\n defaultJobOptions: { priority: 10 }, // Default priority\n})\n\n// VIP customer report: high priority\nawait reportQueue.add(\n 'generate-report',\n { customerId, reportType },\n { priority: 1 } // Lower number = higher priority\n)\n\n// Background analytics: low priority\nawait reportQueue.add(\n 'generate-analytics',\n { period: 'monthly' },\n { priority: 100 }\n)\n",[741,12638,12639,12659,12663,12675,12679,12683,12688,12699,12706,12711,12722,12726,12730,12735,12745,12752,12762,12770],{"__ignoreMap":239},[850,12640,12641,12643,12646,12648,12650,12652,12654,12657],{"class":852,"line":853},[850,12642,3123],{"class":1660},[850,12644,12645],{"class":862}," reportQueue",[850,12647,1743],{"class":1660},[850,12649,3131],{"class":1660},[850,12651,11231],{"class":1667},[850,12653,1766],{"class":856},[850,12655,12656],{"class":877},"'reports'",[850,12658,5281],{"class":856},[850,12660,12661],{"class":852,"line":243},[850,12662,11249],{"class":856},[850,12664,12665,12668,12670,12672],{"class":852,"line":240},[850,12666,12667],{"class":856}," defaultJobOptions: { priority: ",[850,12669,4863],{"class":862},[850,12671,4725],{"class":856},[850,12673,12674],{"class":1427},"// Default priority\n",[850,12676,12677],{"class":852,"line":884},[850,12678,2702],{"class":856},[850,12680,12681],{"class":852,"line":897},[850,12682,2650],{"emptyLinePlaceholder":266},[850,12684,12685],{"class":852,"line":910},[850,12686,12687],{"class":1427},"// VIP customer report: high priority\n",[850,12689,12690,12692,12695,12697],{"class":852,"line":921},[850,12691,11415],{"class":1660},[850,12693,12694],{"class":856}," reportQueue.",[850,12696,11421],{"class":1667},[850,12698,1671],{"class":856},[850,12700,12701,12704],{"class":852,"line":268},[850,12702,12703],{"class":877}," 'generate-report'",[850,12705,881],{"class":856},[850,12707,12708],{"class":852,"line":1338},[850,12709,12710],{"class":856}," { customerId, reportType },\n",[850,12712,12713,12715,12717,12719],{"class":852,"line":1495},[850,12714,11546],{"class":856},[850,12716,3976],{"class":862},[850,12718,3736],{"class":856},[850,12720,12721],{"class":1427},"// Lower number = higher priority\n",[850,12723,12724],{"class":852,"line":1503},[850,12725,1772],{"class":856},[850,12727,12728],{"class":852,"line":1514},[850,12729,2650],{"emptyLinePlaceholder":266},[850,12731,12732],{"class":852,"line":1522},[850,12733,12734],{"class":1427},"// Background analytics: low priority\n",[850,12736,12737,12739,12741,12743],{"class":852,"line":1530},[850,12738,11415],{"class":1660},[850,12740,12694],{"class":856},[850,12742,11421],{"class":1667},[850,12744,1671],{"class":856},[850,12746,12747,12750],{"class":852,"line":1541},[850,12748,12749],{"class":877}," 'generate-analytics'",[850,12751,881],{"class":856},[850,12753,12754,12757,12760],{"class":852,"line":1548},[850,12755,12756],{"class":856}," { period: ",[850,12758,12759],{"class":877},"'monthly'",[850,12761,4720],{"class":856},[850,12763,12764,12766,12768],{"class":852,"line":1555},[850,12765,11546],{"class":856},[850,12767,3148],{"class":862},[850,12769,924],{"class":856},[850,12771,12772],{"class":852,"line":1562},[850,12773,1772],{"class":856},[15,12775,12777],{"id":12776},"bullmq-flow-job-chains-and-pipelines","BullMQ Flow: Job Chains and Pipelines",[20,12779,12780],{},"For multi-step workflows where jobs depend on each other:",[734,12782,12784],{"className":1646,"code":12783,"language":1648,"meta":239,"style":239},"import { FlowProducer } from 'bullmq'\n\nConst flow = new FlowProducer({ connection: redis })\n\n// Create a data import pipeline\nawait flow.add({\n name: 'validate-and-import',\n queueName: 'validation',\n data: { fileId },\n children: [\n {\n name: 'process-data',\n queueName: 'processing',\n data: { fileId },\n children: [\n {\n name: 'generate-report',\n queueName: 'reporting',\n data: { fileId },\n },\n ],\n },\n ],\n})\n",[741,12785,12786,12797,12801,12816,12820,12825,12836,12846,12856,12861,12866,12870,12879,12888,12892,12896,12900,12909,12918,12922,12926,12931,12935,12939],{"__ignoreMap":239},[850,12787,12788,12790,12793,12795],{"class":852,"line":853},[850,12789,3546],{"class":1660},[850,12791,12792],{"class":856}," { FlowProducer } ",[850,12794,3552],{"class":1660},[850,12796,11163],{"class":877},[850,12798,12799],{"class":852,"line":243},[850,12800,2650],{"emptyLinePlaceholder":266},[850,12802,12803,12806,12808,12810,12813],{"class":852,"line":240},[850,12804,12805],{"class":856},"Const flow ",[850,12807,3251],{"class":1660},[850,12809,3131],{"class":1660},[850,12811,12812],{"class":1667}," FlowProducer",[850,12814,12815],{"class":856},"({ connection: redis })\n",[850,12817,12818],{"class":852,"line":884},[850,12819,2650],{"emptyLinePlaceholder":266},[850,12821,12822],{"class":852,"line":897},[850,12823,12824],{"class":1427},"// Create a data import pipeline\n",[850,12826,12827,12829,12832,12834],{"class":852,"line":910},[850,12828,11415],{"class":1660},[850,12830,12831],{"class":856}," flow.",[850,12833,11421],{"class":1667},[850,12835,2780],{"class":856},[850,12837,12838,12841,12844],{"class":852,"line":921},[850,12839,12840],{"class":856}," name: ",[850,12842,12843],{"class":877},"'validate-and-import'",[850,12845,881],{"class":856},[850,12847,12848,12851,12854],{"class":852,"line":268},[850,12849,12850],{"class":856}," queueName: ",[850,12852,12853],{"class":877},"'validation'",[850,12855,881],{"class":856},[850,12857,12858],{"class":852,"line":1338},[850,12859,12860],{"class":856}," data: { fileId },\n",[850,12862,12863],{"class":852,"line":1495},[850,12864,12865],{"class":856}," children: [\n",[850,12867,12868],{"class":852,"line":1503},[850,12869,1849],{"class":856},[850,12871,12872,12874,12877],{"class":852,"line":1514},[850,12873,12840],{"class":856},[850,12875,12876],{"class":877},"'process-data'",[850,12878,881],{"class":856},[850,12880,12881,12883,12886],{"class":852,"line":1522},[850,12882,12850],{"class":856},[850,12884,12885],{"class":877},"'processing'",[850,12887,881],{"class":856},[850,12889,12890],{"class":852,"line":1530},[850,12891,12860],{"class":856},[850,12893,12894],{"class":852,"line":1541},[850,12895,12865],{"class":856},[850,12897,12898],{"class":852,"line":1548},[850,12899,1849],{"class":856},[850,12901,12902,12904,12907],{"class":852,"line":1555},[850,12903,12840],{"class":856},[850,12905,12906],{"class":877},"'generate-report'",[850,12908,881],{"class":856},[850,12910,12911,12913,12916],{"class":852,"line":1562},[850,12912,12850],{"class":856},[850,12914,12915],{"class":877},"'reporting'",[850,12917,881],{"class":856},[850,12919,12920],{"class":852,"line":1572},[850,12921,12860],{"class":856},[850,12923,12924],{"class":852,"line":1580},[850,12925,4720],{"class":856},[850,12927,12928],{"class":852,"line":1590},[850,12929,12930],{"class":856}," ],\n",[850,12932,12933],{"class":852,"line":1597},[850,12934,4720],{"class":856},[850,12936,12937],{"class":852,"line":1604},[850,12938,12930],{"class":856},[850,12940,12941],{"class":852,"line":1611},[850,12942,2702],{"class":856},[20,12944,12945],{},"Child jobs run first. Parent jobs wait for all children to complete. If a child fails, the parent is not started.",[15,12947,12949],{"id":12948},"bull-board-monitoring-dashboard","Bull Board: Monitoring Dashboard",[20,12951,12952],{},"Install Bull Board for a visual dashboard of your queues:",[734,12954,12956],{"className":1646,"code":12955,"language":1648,"meta":239,"style":239},"import { createBullBoard } from '@bull-board/api'\nimport { BullMQAdapter } from '@bull-board/api/bullMQAdapter'\nimport { HonoAdapter } from '@bull-board/hono'\n\nConst serverAdapter = new HonoAdapter()\n\nCreateBullBoard({\n queues: [\n new BullMQAdapter(emailQueue),\n new BullMQAdapter(pdfQueue),\n new BullMQAdapter(imageQueue),\n ],\n serverAdapter,\n})\n\nApp.route('/admin/queues', serverAdapter.registerPlugin())\n",[741,12957,12958,12970,12982,12994,12998,13012,13016,13023,13028,13038,13047,13056,13060,13065,13069,13073],{"__ignoreMap":239},[850,12959,12960,12962,12965,12967],{"class":852,"line":853},[850,12961,3546],{"class":1660},[850,12963,12964],{"class":856}," { createBullBoard } ",[850,12966,3552],{"class":1660},[850,12968,12969],{"class":877}," '@bull-board/api'\n",[850,12971,12972,12974,12977,12979],{"class":852,"line":243},[850,12973,3546],{"class":1660},[850,12975,12976],{"class":856}," { BullMQAdapter } ",[850,12978,3552],{"class":1660},[850,12980,12981],{"class":877}," '@bull-board/api/bullMQAdapter'\n",[850,12983,12984,12986,12989,12991],{"class":852,"line":240},[850,12985,3546],{"class":1660},[850,12987,12988],{"class":856}," { HonoAdapter } ",[850,12990,3552],{"class":1660},[850,12992,12993],{"class":877}," '@bull-board/hono'\n",[850,12995,12996],{"class":852,"line":884},[850,12997,2650],{"emptyLinePlaceholder":266},[850,12999,13000,13003,13005,13007,13010],{"class":852,"line":897},[850,13001,13002],{"class":856},"Const serverAdapter ",[850,13004,3251],{"class":1660},[850,13006,3131],{"class":1660},[850,13008,13009],{"class":1667}," HonoAdapter",[850,13011,2614],{"class":856},[850,13013,13014],{"class":852,"line":910},[850,13015,2650],{"emptyLinePlaceholder":266},[850,13017,13018,13021],{"class":852,"line":921},[850,13019,13020],{"class":1667},"CreateBullBoard",[850,13022,2780],{"class":856},[850,13024,13025],{"class":852,"line":268},[850,13026,13027],{"class":856}," queues: [\n",[850,13029,13030,13032,13035],{"class":852,"line":1338},[850,13031,3131],{"class":1660},[850,13033,13034],{"class":1667}," BullMQAdapter",[850,13036,13037],{"class":856},"(emailQueue),\n",[850,13039,13040,13042,13044],{"class":852,"line":1495},[850,13041,3131],{"class":1660},[850,13043,13034],{"class":1667},[850,13045,13046],{"class":856},"(pdfQueue),\n",[850,13048,13049,13051,13053],{"class":852,"line":1503},[850,13050,3131],{"class":1660},[850,13052,13034],{"class":1667},[850,13054,13055],{"class":856},"(imageQueue),\n",[850,13057,13058],{"class":852,"line":1514},[850,13059,12930],{"class":856},[850,13061,13062],{"class":852,"line":1522},[850,13063,13064],{"class":856}," serverAdapter,\n",[850,13066,13067],{"class":852,"line":1530},[850,13068,2702],{"class":856},[850,13070,13071],{"class":852,"line":1541},[850,13072,2650],{"emptyLinePlaceholder":266},[850,13074,13075,13077,13080,13082,13085,13088,13091],{"class":852,"line":1548},[850,13076,5166],{"class":856},[850,13078,13079],{"class":1667},"route",[850,13081,1766],{"class":856},[850,13083,13084],{"class":877},"'/admin/queues'",[850,13086,13087],{"class":856},", serverAdapter.",[850,13089,13090],{"class":1667},"registerPlugin",[850,13092,13093],{"class":856},"())\n",[20,13095,13096],{},"Protect this route with admin authentication. The dashboard shows queue depth, job throughput, failure rates, and lets you manually retry or delete jobs.",[15,13098,13100],{"id":13099},"graceful-shutdown","Graceful Shutdown",[20,13102,13103],{},"Workers should finish in-progress jobs before shutting down:",[734,13105,13107],{"className":1646,"code":13106,"language":1648,"meta":239,"style":239},"async function shutdown() {\n console.log('Shutting down workers...')\n\n await emailWorker.close()\n await pdfWorker.close()\n\n console.log('Workers stopped gracefully')\n process.exit(0)\n}\n\nProcess.on('SIGTERM', shutdown)\nprocess.on('SIGINT', shutdown)\n",[741,13108,13109,13121,13134,13138,13150,13161,13165,13178,13192,13196,13200,13215],{"__ignoreMap":239},[850,13110,13111,13113,13115,13118],{"class":852,"line":853},[850,13112,1661],{"class":1660},[850,13114,1664],{"class":1660},[850,13116,13117],{"class":1667}," shutdown",[850,13119,13120],{"class":856},"() {\n",[850,13122,13123,13125,13127,13129,13132],{"class":852,"line":243},[850,13124,5662],{"class":856},[850,13126,11816],{"class":1667},[850,13128,1766],{"class":856},[850,13130,13131],{"class":877},"'Shutting down workers...'",[850,13133,1772],{"class":856},[850,13135,13136],{"class":852,"line":240},[850,13137,2650],{"emptyLinePlaceholder":266},[850,13139,13140,13142,13145,13148],{"class":852,"line":884},[850,13141,1746],{"class":1660},[850,13143,13144],{"class":856}," emailWorker.",[850,13146,13147],{"class":1667},"close",[850,13149,2614],{"class":856},[850,13151,13152,13154,13157,13159],{"class":852,"line":897},[850,13153,1746],{"class":1660},[850,13155,13156],{"class":856}," pdfWorker.",[850,13158,13147],{"class":1667},[850,13160,2614],{"class":856},[850,13162,13163],{"class":852,"line":910},[850,13164,2650],{"emptyLinePlaceholder":266},[850,13166,13167,13169,13171,13173,13176],{"class":852,"line":921},[850,13168,5662],{"class":856},[850,13170,11816],{"class":1667},[850,13172,1766],{"class":856},[850,13174,13175],{"class":877},"'Workers stopped gracefully'",[850,13177,1772],{"class":856},[850,13179,13180,13183,13186,13188,13190],{"class":852,"line":268},[850,13181,13182],{"class":856}," process.",[850,13184,13185],{"class":1667},"exit",[850,13187,1766],{"class":856},[850,13189,4039],{"class":862},[850,13191,1772],{"class":856},[850,13193,13194],{"class":852,"line":1338},[850,13195,929],{"class":856},[850,13197,13198],{"class":852,"line":1495},[850,13199,2650],{"emptyLinePlaceholder":266},[850,13201,13202,13205,13207,13209,13212],{"class":852,"line":1503},[850,13203,13204],{"class":856},"Process.",[850,13206,7577],{"class":1667},[850,13208,1766],{"class":856},[850,13210,13211],{"class":877},"'SIGTERM'",[850,13213,13214],{"class":856},", shutdown)\n",[850,13216,13217,13220,13222,13224,13227],{"class":852,"line":1514},[850,13218,13219],{"class":856},"process.",[850,13221,7577],{"class":1667},[850,13223,1766],{"class":856},[850,13225,13226],{"class":877},"'SIGINT'",[850,13228,13214],{"class":856},[20,13230,13231,13234],{},[741,13232,13233],{},"worker.close()"," stops accepting new jobs and waits for current jobs to complete before returning.",[15,13236,13238],{"id":13237},"deployment-considerations","Deployment Considerations",[20,13240,13241],{},"Run workers as separate processes from your API server. This allows:",[211,13243,13244,13247,13250],{},[214,13245,13246],{},"Independent scaling (more workers for high-volume queues)",[214,13248,13249],{},"Separate restarts (worker crash does not affect API)",[214,13251,13252],{},"Per-worker resource configuration (more memory for image processing workers)",[20,13254,13255],{},"In Docker, a separate service per worker type:",[734,13257,13259],{"className":1418,"code":13258,"language":1420,"meta":239,"style":239},"# docker-compose.yml\nservices:\n api:\n build: . Command: node dist/api.js\n\n email-worker:\n build: . Command: node dist/workers/email.js\n scale: 2 # Two instances for redundancy\n\n pdf-worker:\n build: . Command: node dist/workers/pdf.js\n environment:\n - WORKER_CONCURRENCY=2 # PDF is memory-intensive\n",[741,13260,13261,13266,13273,13280,13295,13299,13306,13319,13332,13336,13343,13356,13363],{"__ignoreMap":239},[850,13262,13263],{"class":852,"line":853},[850,13264,13265],{"class":1427},"# docker-compose.yml\n",[850,13267,13268,13271],{"class":852,"line":243},[850,13269,13270],{"class":1433},"services",[850,13272,1437],{"class":856},[850,13274,13275,13278],{"class":852,"line":240},[850,13276,13277],{"class":1433}," api",[850,13279,1437],{"class":856},[850,13281,13282,13285,13287,13290,13292],{"class":852,"line":884},[850,13283,13284],{"class":1433}," build",[850,13286,874],{"class":856},[850,13288,13289],{"class":1433},". Command",[850,13291,874],{"class":856},[850,13293,13294],{"class":877},"node dist/api.js\n",[850,13296,13297],{"class":852,"line":897},[850,13298,2650],{"emptyLinePlaceholder":266},[850,13300,13301,13304],{"class":852,"line":910},[850,13302,13303],{"class":1433}," email-worker",[850,13305,1437],{"class":856},[850,13307,13308,13310,13312,13314,13316],{"class":852,"line":921},[850,13309,13284],{"class":1433},[850,13311,874],{"class":856},[850,13313,13289],{"class":1433},[850,13315,874],{"class":856},[850,13317,13318],{"class":877},"node dist/workers/email.js\n",[850,13320,13321,13324,13326,13329],{"class":852,"line":268},[850,13322,13323],{"class":1433}," scale",[850,13325,874],{"class":856},[850,13327,13328],{"class":862},"2",[850,13330,13331],{"class":1427}," # Two instances for redundancy\n",[850,13333,13334],{"class":852,"line":1338},[850,13335,2650],{"emptyLinePlaceholder":266},[850,13337,13338,13341],{"class":852,"line":1495},[850,13339,13340],{"class":1433}," pdf-worker",[850,13342,1437],{"class":856},[850,13344,13345,13347,13349,13351,13353],{"class":852,"line":1503},[850,13346,13284],{"class":1433},[850,13348,874],{"class":856},[850,13350,13289],{"class":1433},[850,13352,874],{"class":856},[850,13354,13355],{"class":877},"node dist/workers/pdf.js\n",[850,13357,13358,13361],{"class":852,"line":1514},[850,13359,13360],{"class":1433}," environment",[850,13362,1437],{"class":856},[850,13364,13365,13368,13371],{"class":852,"line":1522},[850,13366,13367],{"class":856}," - ",[850,13369,13370],{"class":877},"WORKER_CONCURRENCY=2",[850,13372,13373],{"class":1427}," # PDF is memory-intensive\n",[20,13375,13376],{},"Background jobs are infrastructure you build once and rely on continuously. Design them with failure in mind from the start and you will sleep better when things inevitably go wrong.",[30,13378],{},[20,13380,13381,13382,778],{},"Designing a background job architecture or migrating from a brittle in-process approach to a proper queue? I can help design a system that scales. Book a call: ",[197,13383,3424],{"href":199,"rel":13384},[201],[30,13386],{},[15,13388,209],{"id":208},[211,13390,13391,13395,13401,13405],{},[214,13392,13393],{},[197,13394,3436],{"href":3435},[214,13396,13397],{},[197,13398,13400],{"href":13399},"/blog/building-webhook-system","Building a Reliable Webhook System: Delivery Guarantees and Failure Handling",[214,13402,13403],{},[197,13404,1349],{"href":2160},[214,13406,13407],{},[197,13408,2127],{"href":2126},[1309,13410,2142],{},{"title":239,"searchDepth":240,"depth":240,"links":13412},[13413,13414,13415,13416,13417,13418,13419,13420,13421,13422,13423,13424,13425,13426],{"id":10803,"depth":243,"text":10804},{"id":10837,"depth":243,"text":10838},{"id":10865,"depth":243,"text":10866},{"id":11140,"depth":243,"text":11141},{"id":11399,"depth":243,"text":11400},{"id":11682,"depth":243,"text":11683},{"id":12235,"depth":243,"text":12236},{"id":12396,"depth":243,"text":12397},{"id":12629,"depth":243,"text":12630},{"id":12776,"depth":243,"text":12777},{"id":12948,"depth":243,"text":12949},{"id":13099,"depth":243,"text":13100},{"id":13237,"depth":243,"text":13238},{"id":208,"depth":243,"text":209},"A complete guide to background job processing in Node.js — BullMQ, job queues, worker processes, priority queues, rate limiting, and the failure recovery patterns that matter in production.",[13429,13430],"background jobs Node.js","job queue",{},"/blog/background-jobs-nodejs",{"title":10791,"description":13427},"blog/background-jobs-nodejs",[13436,13437,1328],"Node.js","Background Jobs","5Q83u5yMblEn8H3iwPgusCnsQYHcMDhi1Gwq1TK3H64",[13440,13442,13443,13444,13445,13447,13448,13449,13450,13451,13452,13453,13454,13455,13456,13457,13458,13459,13460,13461,13462,13463,13464,13465,13466,13467,13468,13469,13470,13471,13472,13473,13474,13475,13476,13477,13478,13479,13480,13481,13482,13483,13484,13485,13486,13487,13488,13489,13490,13491,13493,13494,13495,13496,13497,13498,13499,13500,13501,13502,13503,13504,13505,13506,13507,13508,13509,13510,13511,13512,13513,13514,13515,13516,13517,13518,13519,13520,13521,13522,13523,13524,13526,13527,13528,13529,13530,13531,13532,13533,13534,13535,13536,13537,13538,13539,13540,13541,13542,13543,13544,13545,13546,13547,13548,13549,13550,13551,13552,13553,13554,13555,13556,13557,13558,13559,13560,13561,13562,13563,13564,13565,13566,13567,13568,13569,13570,13571,13572,13573,13574,13575,13576,13577,13578,13579,13580,13581,13582,13583,13584,13585,13586,13587,13588,13589,13590,13591,13592,13593,13594,13595,13596,13597,13598,13599,13600,13601,13602,13603,13604,13605,13606,13607,13608,13609,13610,13611,13612,13613,13614,13615,13616,13617,13618,13619,13620,13621,13622,13623,13624,13625,13626,13627,13628,13629,13630,13631,13632,13633,13634,13635,13636,13637,13638,13639,13640,13641,13642,13643,13644,13645,13646,13647,13648,13649,13650,13651,13652,13653,13654,13655,13656,13657,13658,13659,13660,13661,13662,13663,13664,13665,13666,13667,13668,13669,13670,13671,13672,13673,13674,13675,13676,13677,13678,13679,13680,13681,13682,13683,13684,13685,13686,13687,13688,13689,13690,13691,13692,13693,13694,13695,13696,13697,13698,13699,13700,13701,13702,13703,13704,13705,13706,13707,13708,13709,13710,13711,13712,13713,13714,13715,13716,13717,13718,13719,13720,13721,13722,13723,13724,13725,13726,13727,13728,13729,13730,13731,13732,13733,13734,13735,13736,13737,13738,13739,13740,13741,13742,13743,13744,13745,13746,13747,13748,13749,13750,13751,13752,13753,13754,13755,13756,13757,13758,13759,13760,13761,13762,13763,13764,13765,13766,13767,13768,13769,13770,13771,13772,13773,13774,13775,13776,13777,13778,13779,13780,13781,13782,13783,13784,13785,13786,13787,13788,13789,13790,13791,13792,13793,13794,13795,13796,13797,13798,13799,13800,13801,13802,13803,13804,13805,13806,13807,13808,13809,13810,13811,13812,13813,13814,13815,13816,13817,13818,13819,13820,13821,13822,13823,13824,13825,13826,13827,13828,13829,13830,13831,13832,13833,13834,13835,13836,13837,13838,13839,13840,13841,13842,13843,13844,13845,13846,13847,13848,13849,13850,13851,13852,13853,13854,13855,13856,13857,13858,13859,13860,13861,13862,13863,13864,13865,13866,13867,13868,13869,13870,13871,13872,13873,13874,13875,13876,13877,13878,13879,13880,13881,13882,13883,13884,13885,13886,13887,13888,13889,13890,13891,13892,13893,13894,13895,13896,13897,13898,13899,13900,13901,13902,13903,13904,13905,13906,13907,13908,13909,13910,13911,13912,13913,13914,13916,13917,13918,13919,13920,13921,13922,13923,13924,13925,13926,13927,13928,13929,13930,13931,13932,13933,13934,13935,13936,13937,13938,13939,13940,13941,13942,13943,13944,13945,13946,13947,13948,13949,13950,13951,13952,13953,13954,13955,13956,13957,13958,13959,13960,13961,13962,13963,13964,13965,13966,13967,13968,13969,13970,13971,13972,13973,13974,13975,13976,13977,13978,13979,13980,13981,13982,13983,13984,13985,13986,13987,13988,13989,13990,13991,13992,13993,13994,13995,13996,13997,13998,13999,14000,14001,14002,14003,14004,14005,14006,14007,14008,14009,14010,14011,14012,14013,14014,14015,14016,14017,14018,14019,14020,14021,14022,14023,14024,14025,14026,14027,14028,14029,14030,14031,14032,14033,14034,14035,14036,14037,14038,14039,14040,14041,14042,14043,14044,14045,14046,14047,14048,14049,14050,14051,14052,14053,14054,14055,14056,14057,14058,14059,14060,14061,14062,14063,14064,14065,14066,14067,14068,14069,14070,14071,14072,14073,14074,14075,14076,14077,14078,14079,14080,14081,14082,14083,14084],{"category":13441},"Frontend",{"category":7973},{"category":256},{"category":2154},{"category":13446},"Business",{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":256},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":1328},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":5858},{"category":5858},{"category":13446},{"category":13446},{"category":7973},{"category":5858},{"category":7973},{"category":1328},{"category":5858},{"category":2154},{"category":13446},{"category":13492},"DevOps",{"category":256},{"category":7973},{"category":2154},{"category":1328},{"category":2154},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":13492},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":2154},{"category":13525},"Career",{"category":256},{"category":256},{"category":13446},{"category":1328},{"category":13446},{"category":2154},{"category":2154},{"category":13446},{"category":2154},{"category":1328},{"category":2154},{"category":13492},{"category":13492},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":256},{"category":1328},{"category":13446},{"category":13492},{"category":13492},{"category":13492},{"category":7973},{"category":2154},{"category":2154},{"category":7973},{"category":13441},{"category":256},{"category":13492},{"category":13492},{"category":5858},{"category":13492},{"category":13446},{"category":256},{"category":7973},{"category":2154},{"category":7973},{"category":1328},{"category":7973},{"category":1328},{"category":5858},{"category":7973},{"category":7973},{"category":2154},{"category":13446},{"category":2154},{"category":13441},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":13446},{"category":13446},{"category":7973},{"category":13441},{"category":5858},{"category":1328},{"category":5858},{"category":13441},{"category":2154},{"category":2154},{"category":13492},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":13492},{"category":2154},{"category":2154},{"category":7973},{"category":7973},{"category":5858},{"category":1328},{"category":1328},{"category":13525},{"category":13525},{"category":13525},{"category":13446},{"category":2154},{"category":13492},{"category":1328},{"category":7973},{"category":7973},{"category":13492},{"category":1328},{"category":1328},{"category":13441},{"category":2154},{"category":7973},{"category":7973},{"category":2154},{"category":7973},{"category":13492},{"category":13492},{"category":7973},{"category":5858},{"category":7973},{"category":1328},{"category":5858},{"category":1328},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":5858},{"category":2154},{"category":13492},{"category":13492},{"category":13446},{"category":2154},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":1328},{"category":1328},{"category":1328},{"category":2154},{"category":7973},{"category":7973},{"category":7973},{"category":13492},{"category":13446},{"category":7973},{"category":7973},{"category":2154},{"category":7973},{"category":2154},{"category":13441},{"category":7973},{"category":13446},{"category":13446},{"category":2154},{"category":2154},{"category":256},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":2154},{"category":13492},{"category":13492},{"category":13492},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":7973},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":13446},{"category":13446},{"category":7973},{"category":2154},{"category":13441},{"category":1328},{"category":13525},{"category":7973},{"category":7973},{"category":5858},{"category":2154},{"category":7973},{"category":7973},{"category":13492},{"category":7973},{"category":13441},{"category":13492},{"category":13492},{"category":5858},{"category":2154},{"category":2154},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":13525},{"category":7973},{"category":1328},{"category":2154},{"category":2154},{"category":7973},{"category":13492},{"category":7973},{"category":7973},{"category":7973},{"category":13441},{"category":7973},{"category":7973},{"category":2154},{"category":7973},{"category":2154},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":256},{"category":256},{"category":2154},{"category":7973},{"category":13492},{"category":13492},{"category":7973},{"category":2154},{"category":7973},{"category":7973},{"category":256},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":2154},{"category":5858},{"category":2154},{"category":2154},{"category":13441},{"category":2154},{"category":13441},{"category":13441},{"category":5858},{"category":1328},{"category":2154},{"category":1328},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":2154},{"category":13446},{"category":2154},{"category":2154},{"category":7973},{"category":1328},{"category":256},{"category":256},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":13446},{"category":2154},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":13441},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":2154},{"category":1328},{"category":7973},{"category":13446},{"category":256},{"category":7973},{"category":13446},{"category":5858},{"category":7973},{"category":5858},{"category":2154},{"category":13492},{"category":7973},{"category":7973},{"category":2154},{"category":7973},{"category":1328},{"category":7973},{"category":7973},{"category":2154},{"category":13446},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":13446},{"category":2154},{"category":2154},{"category":13446},{"category":13492},{"category":2154},{"category":256},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":7973},{"category":7973},{"category":7973},{"category":256},{"category":2154},{"category":2154},{"category":1328},{"category":13441},{"category":2154},{"category":7973},{"category":2154},{"category":1328},{"category":13446},{"category":13446},{"category":13441},{"category":13441},{"category":7973},{"category":13446},{"category":5858},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":2154},{"category":2154},{"category":1328},{"category":2154},{"category":2154},{"category":2154},{"category":13915},"Programming",{"category":2154},{"category":2154},{"category":1328},{"category":1328},{"category":2154},{"category":2154},{"category":13446},{"category":5858},{"category":2154},{"category":13446},{"category":2154},{"category":2154},{"category":2154},{"category":2154},{"category":13492},{"category":1328},{"category":13446},{"category":13446},{"category":2154},{"category":2154},{"category":13446},{"category":2154},{"category":5858},{"category":13446},{"category":2154},{"category":2154},{"category":1328},{"category":1328},{"category":7973},{"category":13446},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":13441},{"category":7973},{"category":13492},{"category":5858},{"category":5858},{"category":5858},{"category":5858},{"category":5858},{"category":5858},{"category":7973},{"category":2154},{"category":13492},{"category":1328},{"category":13492},{"category":1328},{"category":2154},{"category":13441},{"category":7973},{"category":1328},{"category":13441},{"category":7973},{"category":7973},{"category":7973},{"category":1328},{"category":1328},{"category":1328},{"category":13446},{"category":13446},{"category":13446},{"category":1328},{"category":1328},{"category":13446},{"category":13446},{"category":13446},{"category":7973},{"category":5858},{"category":2154},{"category":13492},{"category":2154},{"category":7973},{"category":13446},{"category":13446},{"category":7973},{"category":7973},{"category":1328},{"category":2154},{"category":1328},{"category":1328},{"category":1328},{"category":13441},{"category":2154},{"category":7973},{"category":7973},{"category":13446},{"category":13446},{"category":1328},{"category":2154},{"category":13525},{"category":1328},{"category":13525},{"category":13446},{"category":7973},{"category":1328},{"category":7973},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":7973},{"category":256},{"category":256},{"category":13492},{"category":7973},{"category":7973},{"category":7973},{"category":7973},{"category":2154},{"category":2154},{"category":13441},{"category":2154},{"category":5858},{"category":1328},{"category":13441},{"category":13441},{"category":2154},{"category":2154},{"category":13441},{"category":13441},{"category":13441},{"category":5858},{"category":2154},{"category":2154},{"category":13446},{"category":2154},{"category":1328},{"category":7973},{"category":7973},{"category":1328},{"category":7973},{"category":7973},{"category":1328},{"category":7973},{"category":2154},{"category":7973},{"category":5858},{"category":7973},{"category":7973},{"category":7973},{"category":13492},{"category":13492},{"category":5858},1772951194491]