[{"data":1,"prerenderedAt":8333},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-7":4,"blog-paginated-cats":7690},640,[5,763,986,1501,1687,2849,3355,4130,4479,4712,5475,5757,5970,6649,6963],{"id":6,"title":7,"author":8,"body":11,"category":745,"date":746,"description":747,"extension":748,"featured":749,"image":750,"keywords":751,"meta":754,"navigation":458,"path":755,"readTime":265,"seo":756,"stem":757,"tags":758,"__hash__":762},"blog/blog/font-loading-optimization.md","Font Loading Optimization: Eliminating Layout Shift and Invisible Text",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":733},"minimark",[14,19,23,30,36,39,42,51,56,145,154,161,169,177,184,186,190,196,199,269,275,281,287,289,293,300,303,322,328,338,340,344,347,350,353,494,497,507,509,513,516,519,598,601,603,607,621,637,646,648,652,655,681,684,686,697,699,703,729],[15,16,18],"h2",{"id":17},"the-two-font-loading-problems-that-hurt-performance","The Two Font Loading Problems That Hurt Performance",[20,21,22],"p",{},"Web fonts create two distinct user experience problems that both affect Core Web Vitals scores:",[20,24,25,29],{},[26,27,28],"strong",{},"FOIT (Flash of Invisible Text):"," The browser blocks text rendering until the web font has downloaded. Users see blank spaces where text should be. On a slow connection, critical content like headlines can be invisible for 2-3 seconds.",[20,31,32,35],{},[26,33,34],{},"FOUT (Flash of Unstyled Text):"," The browser renders text immediately using a fallback system font, then swaps to the web font when it downloads. This causes the text to visibly shift in size and position — a layout shift event that accumulates into your CLS score.",[20,37,38],{},"Every web font implementation involves a trade-off between these two problems. The goal of font loading optimization is to make that trade-off as small as possible through a combination of better loading strategies and fallback font tuning.",[40,41],"hr",{},[15,43,45,46,50],{"id":44},"the-font-display-property","The ",[47,48,49],"code",{},"font-display"," Property",[20,52,53,55],{},[47,54,49],{}," is the CSS property that controls what the browser does while a font is loading. Understanding the options is the foundation of font loading optimization:",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-css shiki shiki-themes github-dark","@font-face {\n font-family: 'Geist';\n src: url('/fonts/geist.woff2') format('woff2');\n font-display: swap;\n}\n","css","",[47,64,65,78,95,126,139],{"__ignoreMap":62},[66,67,70,74],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"snl16","@font-face",[66,75,77],{"class":76},"s95oV"," {\n",[66,79,81,85,88,92],{"class":68,"line":80},2,[66,82,84],{"class":83},"sDLfK"," font-family",[66,86,87],{"class":76},": ",[66,89,91],{"class":90},"sU2Wk","'Geist'",[66,93,94],{"class":76},";\n",[66,96,98,101,103,106,109,112,115,118,120,123],{"class":68,"line":97},3,[66,99,100],{"class":83}," src",[66,102,87],{"class":76},[66,104,105],{"class":83},"url",[66,107,108],{"class":76},"(",[66,110,111],{"class":90},"'/fonts/geist.woff2'",[66,113,114],{"class":76},") ",[66,116,117],{"class":83},"format",[66,119,108],{"class":76},[66,121,122],{"class":90},"'woff2'",[66,124,125],{"class":76},");\n",[66,127,129,132,134,137],{"class":68,"line":128},4,[66,130,131],{"class":83}," font-display",[66,133,87],{"class":76},[66,135,136],{"class":83},"swap",[66,138,94],{"class":76},[66,140,142],{"class":68,"line":141},5,[66,143,144],{"class":76},"}\n",[20,146,147,153],{},[26,148,149,152],{},[47,150,151],{},"block",":"," Block text rendering for up to 3 seconds, then swap. This is the default behavior — maximum FOIT, no FOUT. Appropriate for critical icon fonts where rendering the wrong symbol is worse than no symbol.",[20,155,156,160],{},[26,157,158,152],{},[47,159,136],{}," Render immediately with fallback font, swap to web font when available. Zero FOIT, potential FOUT. This is the right default for most body text — users see content immediately, and the font swap is usually brief.",[20,162,163,168],{},[26,164,165,152],{},[47,166,167],{},"fallback"," A short block period (100ms), then fallback font. If the web font loads within 3 seconds, swap. After 3 seconds, use the fallback font permanently for that page load. A reasonable middle ground.",[20,170,171,176],{},[26,172,173,152],{},[47,174,175],{},"optional"," Very short block period, then fallback. The browser may or may not load the web font depending on network conditions. Good for non-essential decorative fonts.",[20,178,179,180,183],{},"For most applications, ",[47,181,182],{},"font-display: swap"," is the correct choice for body and heading fonts. The FOUT is a CLS cost, but it's addressable through fallback font metric matching (covered below). FOIT is not addressable and causes worse user experience.",[40,185],{},[15,187,189],{"id":188},"preloading-critical-fonts","Preloading Critical Fonts",[20,191,192,193,195],{},"By default, the browser discovers font files by parsing the CSS, finding the ",[47,194,73],{}," rules, then encountering elements that use those fonts before initiating the download. This is late in the resource loading process.",[20,197,198],{},"Font preloading moves the download earlier, reducing both FOIT duration and FOUT duration:",[57,200,204],{"className":201,"code":202,"language":203,"meta":62,"style":62},"language-html shiki shiki-themes github-dark","\u003Clink\n rel=\"preload\"\n href=\"/fonts/geist-regular.woff2\"\n as=\"font\"\n type=\"font/woff2\"\n crossorigin\n>\n","html",[47,205,206,215,227,237,247,257,263],{"__ignoreMap":62},[66,207,208,211],{"class":68,"line":69},[66,209,210],{"class":76},"\u003C",[66,212,214],{"class":213},"s4JwU","link\n",[66,216,217,221,224],{"class":68,"line":80},[66,218,220],{"class":219},"svObZ"," rel",[66,222,223],{"class":76},"=",[66,225,226],{"class":90},"\"preload\"\n",[66,228,229,232,234],{"class":68,"line":97},[66,230,231],{"class":219}," href",[66,233,223],{"class":76},[66,235,236],{"class":90},"\"/fonts/geist-regular.woff2\"\n",[66,238,239,242,244],{"class":68,"line":128},[66,240,241],{"class":219}," as",[66,243,223],{"class":76},[66,245,246],{"class":90},"\"font\"\n",[66,248,249,252,254],{"class":68,"line":141},[66,250,251],{"class":219}," type",[66,253,223],{"class":76},[66,255,256],{"class":90},"\"font/woff2\"\n",[66,258,260],{"class":68,"line":259},6,[66,261,262],{"class":219}," crossorigin\n",[66,264,266],{"class":68,"line":265},7,[66,267,268],{"class":76},">\n",[20,270,45,271,274],{},[47,272,273],{},"crossorigin"," attribute is required even for same-origin fonts — the font fetch spec requires it.",[20,276,277,280],{},[26,278,279],{},"What to preload:"," Preload only the fonts that render visible text above the fold. Preloading too many fonts creates bandwidth contention and slows the initial render of more critical resources. The practical limit is usually 2-3 preloads per page.",[20,282,283,286],{},[26,284,285],{},"Preload only the fonts in use:"," If your heading uses a different weight than your body text, preload both weights. Don't preload weights that don't appear in the initial viewport.",[40,288],{},[15,290,292],{"id":291},"self-hosting-vs-google-fonts","Self-Hosting vs Google Fonts",[20,294,295,296,299],{},"Google Fonts is convenient but slower than self-hosting for several reasons: an additional DNS lookup for fonts.googleapis.com and fonts.gstatic.com, no cache benefits on the font files themselves (CDN cache is shared but browser cache is per-origin), and you can't control the loading strategy (no direct ",[47,297,298],{},"preload"," without fetching the CSS first).",[20,301,302],{},"Self-hosting eliminates all three issues. With self-hosting, you can:",[304,305,306,313,316,319],"ul",{},[307,308,309,310,312],"li",{},"Add ",[47,311,298],{}," tags directly for specific font files",[307,314,315],{},"Serve fonts from your own CDN domain (no extra DNS lookup)",[307,317,318],{},"Control compression and caching headers",[307,320,321],{},"Subset fonts to only the characters you need",[20,323,324,327],{},[26,325,326],{},"google-webfonts-helper"," (gwfh.mranftl.com) makes it easy to download Google Fonts for self-hosting with the correct CSS and WOFF2 files.",[20,329,330,333,334,337],{},[26,331,332],{},"Font subsetting:"," If you're using Latin characters only, you don't need the full font file with Cyrillic, Greek, and Vietnamese character ranges. Subsetting removes unused character ranges. ",[47,335,336],{},"pyftsubset"," (fonttools) or Glyphhanger can subset fonts programmatically. Typical reduction: 50-70% smaller files.",[40,339],{},[15,341,343],{"id":342},"fallback-font-metric-matching-eliminating-cls-from-fout","Fallback Font Metric Matching: Eliminating CLS from FOUT",[20,345,346],{},"This is the most sophisticated font optimization technique and the one that most developers skip — to the detriment of their CLS scores.",[20,348,349],{},"When a browser swaps from a fallback font to a web font, the text reflows if the two fonts have different metrics. The text takes up more or less space, changes height, and everything shifts. The fix is to make the fallback font's metrics match the web font's metrics as closely as possible.",[20,351,352],{},"CSS provides four properties for fallback font metric override:",[57,354,356],{"className":59,"code":355,"language":61,"meta":62,"style":62},"@font-face {\n font-family: 'Geist-fallback';\n src: local('Arial');\n ascent-override: 90%;\n descent-override: 22%;\n line-gap-override: 0%;\n size-adjust: 104%;\n}\n\nBody {\n font-family: 'Geist', 'Geist-fallback', sans-serif;\n}\n",[47,357,358,364,375,391,406,420,434,448,453,460,468,489],{"__ignoreMap":62},[66,359,360,362],{"class":68,"line":69},[66,361,73],{"class":72},[66,363,77],{"class":76},[66,365,366,368,370,373],{"class":68,"line":80},[66,367,84],{"class":83},[66,369,87],{"class":76},[66,371,372],{"class":90},"'Geist-fallback'",[66,374,94],{"class":76},[66,376,377,379,381,384,386,389],{"class":68,"line":97},[66,378,100],{"class":83},[66,380,87],{"class":76},[66,382,383],{"class":83},"local",[66,385,108],{"class":76},[66,387,388],{"class":90},"'Arial'",[66,390,125],{"class":76},[66,392,393,396,398,401,404],{"class":68,"line":128},[66,394,395],{"class":83}," ascent-override",[66,397,87],{"class":76},[66,399,400],{"class":83},"90",[66,402,403],{"class":72},"%",[66,405,94],{"class":76},[66,407,408,411,413,416,418],{"class":68,"line":141},[66,409,410],{"class":83}," descent-override",[66,412,87],{"class":76},[66,414,415],{"class":83},"22",[66,417,403],{"class":72},[66,419,94],{"class":76},[66,421,422,425,427,430,432],{"class":68,"line":259},[66,423,424],{"class":83}," line-gap-override",[66,426,87],{"class":76},[66,428,429],{"class":83},"0",[66,431,403],{"class":72},[66,433,94],{"class":76},[66,435,436,439,441,444,446],{"class":68,"line":265},[66,437,438],{"class":83}," size-adjust",[66,440,87],{"class":76},[66,442,443],{"class":83},"104",[66,445,403],{"class":72},[66,447,94],{"class":76},[66,449,451],{"class":68,"line":450},8,[66,452,144],{"class":76},[66,454,456],{"class":68,"line":455},9,[66,457,459],{"emptyLinePlaceholder":458},true,"\n",[66,461,463,466],{"class":68,"line":462},10,[66,464,465],{"class":213},"Body",[66,467,77],{"class":76},[66,469,471,473,475,477,480,482,484,487],{"class":68,"line":470},11,[66,472,84],{"class":83},[66,474,87],{"class":76},[66,476,91],{"class":90},[66,478,479],{"class":76},", ",[66,481,372],{"class":90},[66,483,479],{"class":76},[66,485,486],{"class":83},"sans-serif",[66,488,94],{"class":76},[66,490,492],{"class":68,"line":491},12,[66,493,144],{"class":76},[20,495,496],{},"The values tell the browser to render Arial with the same metrics as Geist. When Geist loads and swaps in, the text occupies the same space — no layout shift.",[20,498,499,502,503,506],{},[26,500,501],{},"Finding the right values:"," Use the Fontaine library (for Nuxt/Next.js projects) which calculates these values automatically from the font files, or use the ",[47,504,505],{},"fontpie"," tool. Manually tweaking these values is feasible but tedious.",[40,508],{},[15,510,512],{"id":511},"variable-fonts","Variable Fonts",[20,514,515],{},"Variable fonts are a single font file that contains a continuous range of weights, widths, and other axes rather than separate files for each variant. A variable font file is typically smaller than multiple static font files for the same family when you use more than two or three weights.",[20,517,518],{},"If your design uses three or more font weights (light, regular, bold — common for headings plus body), a variable font is almost certainly smaller than the equivalent set of static fonts.",[57,520,522],{"className":59,"code":521,"language":61,"meta":62,"style":62},"@font-face {\n font-family: 'Geist';\n src: url('/fonts/geist-variable.woff2') format('woff2-variations');\n font-weight: 100 900; /* weight range supported */\n font-display: swap;\n}\n",[47,523,524,530,540,564,584,594],{"__ignoreMap":62},[66,525,526,528],{"class":68,"line":69},[66,527,73],{"class":72},[66,529,77],{"class":76},[66,531,532,534,536,538],{"class":68,"line":80},[66,533,84],{"class":83},[66,535,87],{"class":76},[66,537,91],{"class":90},[66,539,94],{"class":76},[66,541,542,544,546,548,550,553,555,557,559,562],{"class":68,"line":97},[66,543,100],{"class":83},[66,545,87],{"class":76},[66,547,105],{"class":83},[66,549,108],{"class":76},[66,551,552],{"class":90},"'/fonts/geist-variable.woff2'",[66,554,114],{"class":76},[66,556,117],{"class":83},[66,558,108],{"class":76},[66,560,561],{"class":90},"'woff2-variations'",[66,563,125],{"class":76},[66,565,566,569,571,574,577,580],{"class":68,"line":128},[66,567,568],{"class":83}," font-weight",[66,570,87],{"class":76},[66,572,573],{"class":83},"100",[66,575,576],{"class":83}," 900",[66,578,579],{"class":76},"; ",[66,581,583],{"class":582},"sAwPA","/* weight range supported */\n",[66,585,586,588,590,592],{"class":68,"line":141},[66,587,131],{"class":83},[66,589,87],{"class":76},[66,591,136],{"class":83},[66,593,94],{"class":76},[66,595,596],{"class":68,"line":259},[66,597,144],{"class":76},[20,599,600],{},"With a variable font, you can use any weight between 100 and 900 without a separate file.",[40,602],{},[15,604,606],{"id":605},"framework-specific-implementations","Framework-Specific Implementations",[20,608,609,612,613,616,617,620],{},[26,610,611],{},"Next.js:"," The ",[47,614,615],{},"next/font"," package handles font self-hosting, subsetting, and fallback metric generation automatically at build time. It generates the ",[47,618,619],{},"size-adjust"," and override values. This is the easiest way to get optimal font loading in a Next.js project.",[20,622,623,612,626,629,630,632,633,636],{},[26,624,625],{},"Nuxt.js:",[47,627,628],{},"@nuxtjs/fontaine"," module provides similar functionality — automatic fallback metric calculation and ",[47,631,73],{}," injection. The ",[47,634,635],{},"nuxt-fonts"," module adds Google Fonts self-hosting.",[20,638,639,642,643,645],{},[26,640,641],{},"Plain HTML/CSS:"," Self-host the fonts, add preload tags for above-the-fold fonts, use ",[47,644,182],{},", and manually set fallback font metric overrides.",[40,647],{},[15,649,651],{"id":650},"measuring-font-loading-impact","Measuring Font Loading Impact",[20,653,654],{},"Tools to verify your font loading is working correctly:",[304,656,657,663,669,675],{},[307,658,659,662],{},[26,660,661],{},"WebPageTest"," with \"Filmstrip\" view — shows exactly when the font swap occurs visually",[307,664,665,668],{},[26,666,667],{},"Lighthouse"," — flags FOIT and CLS from font swaps",[307,670,671,674],{},[26,672,673],{},"Chrome DevTools Performance tab"," — shows font network requests and swap timing",[307,676,677,680],{},[26,678,679],{},"CrUX data in PageSpeed Insights"," — real user CLS from font swaps will show in field data",[20,682,683],{},"The target: zero CLS attributable to font loading, zero FOIT, and web fonts loaded within 1 second of page start on a good connection.",[40,685],{},[20,687,688,689,696],{},"Font loading is one of those areas where small implementation details make a significant difference to both visual stability and perceived performance. If you're seeing CLS or layout issues on your site related to fonts, book a call at ",[690,691,695],"a",{"href":692,"rel":693},"https://calendly.com/jamesrossjr",[694],"nofollow","calendly.com/jamesrossjr"," and let's diagnose and fix it.",[40,698],{},[15,700,702],{"id":701},"keep-reading","Keep Reading",[304,704,705,711,717,723],{},[307,706,707],{},[690,708,710],{"href":709},"/blog/core-web-vitals-optimization","Core Web Vitals Optimization: A Developer's Complete Guide",[307,712,713],{},[690,714,716],{"href":715},"/blog/image-optimization-web","Image Optimization for the Web: Formats, Compression, and Lazy Loading",[307,718,719],{},[690,720,722],{"href":721},"/blog/frontend-performance-guide","Frontend Performance: The Metrics That Matter and How to Hit Them",[307,724,725],{},[690,726,728],{"href":727},"/blog/api-performance-optimization","API Performance Optimization: Making Your Endpoints Fast at Scale",[730,731,732],"style",{},"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 .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}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":62,"searchDepth":97,"depth":97,"links":734},[735,736,738,739,740,741,742,743,744],{"id":17,"depth":80,"text":18},{"id":44,"depth":80,"text":737},"The font-display Property",{"id":188,"depth":80,"text":189},{"id":291,"depth":80,"text":292},{"id":342,"depth":80,"text":343},{"id":511,"depth":80,"text":512},{"id":605,"depth":80,"text":606},{"id":650,"depth":80,"text":651},{"id":701,"depth":80,"text":702},"Engineering","2026-03-03","Web fonts are a common source of layout shift and invisible text. Here's a complete guide to font loading strategies that eliminate both problems without sacrificing design.","md",false,null,[752,753],"font loading optimization","web fonts performance",{},"/blog/font-loading-optimization",{"title":7,"description":747},"blog/font-loading-optimization",[759,760,761],"Performance","Fonts","Core Web Vitals","5kWHwcAWj2OBDUCi8YNOhaBG7OlT-XLezwAcr1B7GrM",{"id":764,"title":765,"author":766,"body":767,"category":972,"date":746,"description":973,"extension":748,"featured":749,"image":750,"keywords":974,"meta":977,"navigation":458,"path":978,"readTime":265,"seo":979,"stem":980,"tags":981,"__hash__":985},"blog/blog/freelance-developer-vs-agency.md","Freelance Developer vs Software Agency: How to Choose the Right Partner",{"name":9,"bio":10},{"type":12,"value":768,"toc":962},[769,773,776,779,782,784,788,791,794,797,799,803,809,815,821,827,829,833,839,845,851,857,859,863,866,872,878,884,890,892,896,899,902,905,908,911,913,917,920,923,925,932,934,936],[15,770,772],{"id":771},"the-question-that-deserves-a-real-answer","The Question That Deserves a Real Answer",[20,774,775],{},"Every week, someone asks me some version of this question: \"Should I hire a freelancer or go with an agency?\" Usually the person asking is a founder, a business owner, or an internal product manager who has a real project to deliver and isn't sure which type of partner will get them there.",[20,777,778],{},"The frustrating thing is that most of the advice on this topic defaults to one camp or the other. Agencies will tell you freelancers are risky and unaccountable. Freelancers will tell you agencies are bloated and overpriced. Neither is giving you the full picture.",[20,780,781],{},"Here's the actual framework I use when helping clients think through this decision.",[40,783],{},[15,785,787],{"id":786},"what-each-option-actually-is","What Each Option Actually Is",[20,789,790],{},"A freelance developer is a solo professional who handles your project directly. You are contracting with a person, not an organization. The code they write, the decisions they make, and the quality of their communication are entirely a function of that individual.",[20,792,793],{},"A software agency is an organization — anywhere from a boutique shop of five people to a firm of hundreds — that provides development services as a structured business. When you hire an agency, you're getting a process, a team structure, and (ideally) the institutional knowledge and accountability systems that come with a real company.",[20,795,796],{},"Both can deliver excellent work. Both can also deliver disasters. The deciding factors are your project's characteristics, not a blanket rule about which model is superior.",[40,798],{},[15,800,802],{"id":801},"when-a-freelancer-is-the-right-call","When a Freelancer Is the Right Call",[20,804,805,808],{},[26,806,807],{},"Your project is well-defined and scope is stable."," If you know exactly what you want built, have clear requirements, and aren't expecting significant pivots, a skilled freelancer can execute it efficiently without the coordination overhead of an agency team.",[20,810,811,814],{},[26,812,813],{},"You're budget-constrained and can absorb some risk."," Freelancers typically cost less per hour than agencies. The tradeoff is that you have a single point of failure — one person's illness, personal situation, or shifting priorities can derail your project. If that's a risk you can manage, the cost savings are real.",[20,816,817,820],{},[26,818,819],{},"The work is specialized and the person you're hiring is genuinely expert in it."," The best freelancers are often very deep specialists — a particular framework, a specific type of integration, a niche technology stack. If your problem requires that specific depth, finding the right freelancer can get you better work than an agency that has to staff the project with whoever's available.",[20,822,823,826],{},[26,824,825],{},"You need speed and direct communication."," Agencies add process. That's often valuable, but it adds time. Briefing an agency account manager who relays specs to a project manager who hands off to a developer who asks clarifying questions that go back up the chain is slower than sending a Slack message directly to the person writing the code.",[40,828],{},[15,830,832],{"id":831},"when-an-agency-is-the-right-call","When an Agency Is the Right Call",[20,834,835,838],{},[26,836,837],{},"Your project has multiple workstreams happening simultaneously."," Backend, frontend, design, DevOps — if these need to happen in parallel, you need a team. Asking a freelancer to wear all of those hats usually ends in one of two outcomes: they're a generalist who does all of them adequately but none of them well, or they're actually great at one and quietly terrible at the others.",[20,840,841,844],{},[26,842,843],{},"Continuity matters more than cost."," If your project needs ongoing maintenance, feature development, or 24/7 support, an agency can provide staffing continuity that a freelancer simply cannot. When your developer decides to take a three-month contract somewhere else, what's your plan? An agency has redundancy built in.",[20,846,847,850],{},[26,848,849],{},"You're building something complex or high-stakes."," Regulated industries, financial applications, healthcare software, systems that need to scale — these benefit from an agency's internal review processes, code standards enforcement, and the accountability that comes from a structured organization. You want something more than one person's judgment on decisions that matter.",[20,852,853,856],{},[26,854,855],{},"You need a design system, not just code."," Most freelance developers don't do UI/UX design at a professional level. If you need a coherent product experience — not just functional code — an agency that includes design is worth the premium.",[40,858],{},[15,860,862],{"id":861},"the-questions-that-actually-determine-the-answer","The Questions That Actually Determine the Answer",[20,864,865],{},"Rather than defaulting to a category, ask yourself these:",[20,867,868,871],{},[26,869,870],{},"How clear are my requirements?"," If you're still figuring out what you want to build, an agency with a discovery process is worth the overhead. Freelancers generally work better when the spec is tight.",[20,873,874,877],{},[26,875,876],{},"What's my runway if the engagement goes sideways?"," A bad hire of any kind is painful, but losing a solo freelancer mid-project is a different kind of painful than having an agency under-deliver. Know what each scenario costs you.",[20,879,880,883],{},[26,881,882],{},"How much of my time can I give to this?"," Working with a freelancer typically requires more active management from you. Agencies absorb more of that coordination internally.",[20,885,886,889],{},[26,887,888],{},"What does the first 90 days need to produce?"," If you need a working MVP in 60 days, prioritize whoever has done the most similar work recently — not the broader category they fall into.",[40,891],{},[15,893,895],{"id":894},"how-to-evaluate-either-type-of-partner","How to Evaluate Either Type of Partner",[20,897,898],{},"The vetting process is similar regardless of which direction you go.",[20,900,901],{},"Ask to see work that is as similar to your project as possible. Not just a portfolio — specific case studies where you can ask: what was the stack, what was the timeline, what went wrong and how did they handle it?",[20,903,904],{},"Check references directly. Not LinkedIn recommendations — actual phone calls where you ask specific questions about communication, delivery on estimates, and how they handled problems.",[20,906,907],{},"Run a paid discovery or scoping engagement before committing to the full project. Any experienced developer or agency should be willing to produce a technical spec or architecture document for a fixed fee. That deliverable tells you a lot about how they think and communicate.",[20,909,910],{},"Watch how they handle your early interactions. Are they asking sharp questions about your requirements or just agreeing with everything? Are their estimates thoughtful or immediate? The quality of the conversation before the contract is the best predictor of the quality of the work after it.",[40,912],{},[15,914,916],{"id":915},"the-hybrid-model-people-forget-about","The Hybrid Model People Forget About",[20,918,919],{},"There's a third path worth naming: a senior independent consultant who operates like a fractional technical director. Not a solo freelancer executing tasks, and not a full agency, but someone with the strategic depth to make architectural decisions and manage other contributors — including coordinating freelancers or vendors on specific pieces.",[20,921,922],{},"This model often works well for startups and growing companies that need technical leadership without a full-time hire, or for established businesses that want informed oversight of an outsourced development relationship.",[40,924],{},[20,926,927,928,931],{},"The right answer to freelancer vs agency is always specific to your project, your team's capacity, and what you can absorb if things get hard. If you'd like help thinking through which model fits your situation, book a call at ",[690,929,695],{"href":692,"rel":930},[694]," and let's work through it together.",[40,933],{},[15,935,702],{"id":701},[304,937,938,944,950,956],{},[307,939,940],{},[690,941,943],{"href":942},"/blog/hiring-software-development-company","Hiring a Software Development Company: What to Look For, What to Avoid",[307,945,946],{},[690,947,949],{"href":948},"/blog/pricing-software-projects","Pricing Custom Software Projects: The Framework That Works",[307,951,952],{},[690,953,955],{"href":954},"/blog/remote-software-development","Remote Software Development: How Distributed Teams Can Build Better Products",[307,957,958],{},[690,959,961],{"href":960},"/blog/scope-creep-prevention","Scope Creep Prevention: How to Keep Custom Software Projects on Track",{"title":62,"searchDepth":97,"depth":97,"links":963},[964,965,966,967,968,969,970,971],{"id":771,"depth":80,"text":772},{"id":786,"depth":80,"text":787},{"id":801,"depth":80,"text":802},{"id":831,"depth":80,"text":832},{"id":861,"depth":80,"text":862},{"id":894,"depth":80,"text":895},{"id":915,"depth":80,"text":916},{"id":701,"depth":80,"text":702},"Business","Deciding between a freelance developer and a software agency is one of the most consequential choices in a software project. Here's a framework for getting it right.",[975,976],"freelance developer vs agency","hire software developer",{},"/blog/freelance-developer-vs-agency",{"title":765,"description":973},"blog/freelance-developer-vs-agency",[982,983,984],"Business Strategy","Software Development","Hiring","HOi0Yt1PAg2Ph0yYDeaajy9OjAhIBUrZ4rH1DujL02A",{"id":987,"title":722,"author":988,"body":989,"category":745,"date":746,"description":1491,"extension":748,"featured":749,"image":750,"keywords":1492,"meta":1495,"navigation":458,"path":721,"readTime":265,"seo":1496,"stem":1497,"tags":1498,"__hash__":1500},"blog/blog/frontend-performance-guide.md",{"name":9,"bio":10},{"type":12,"value":990,"toc":1481},[991,995,998,1001,1007,1013,1019,1025,1031,1034,1036,1040,1046,1052,1058,1063,1197,1200,1202,1206,1209,1215,1220,1223,1226,1229,1231,1235,1238,1300,1302,1306,1309,1359,1361,1365,1417,1419,1423,1426,1440,1443,1445,1448,1454,1456,1458,1478],[15,992,994],{"id":993},"the-metrics-that-define-frontend-performance","The Metrics That Define Frontend Performance",[20,996,997],{},"\"Make it faster\" is not an actionable goal. \"Get LCP under 2.5 seconds for the 75th percentile of users\" is an actionable goal. Frontend performance work requires specific, measurable targets before it can be systematically pursued.",[20,999,1000],{},"The metrics that matter in production:",[20,1002,1003,1006],{},[26,1004,1005],{},"Largest Contentful Paint (LCP):"," When does the user first see the main content? Target: under 2.5 seconds.",[20,1008,1009,1012],{},[26,1010,1011],{},"Interaction to Next Paint (INP):"," How responsive are interactions throughout the page lifetime? Target: under 200ms.",[20,1014,1015,1018],{},[26,1016,1017],{},"Cumulative Layout Shift (CLS):"," How much does content unexpectedly move while the user is looking at it? Target: under 0.1.",[20,1020,1021,1024],{},[26,1022,1023],{},"Time to First Byte (TTFB):"," How fast does the server start sending the HTML? Target: under 800ms. This is the foundation — you can't achieve good LCP without good TTFB.",[20,1026,1027,1030],{},[26,1028,1029],{},"Total Blocking Time (TBT):"," A Lighthouse-only metric that approximates main thread blockage during page load. Target: under 200ms. High TBT is the leading indicator of poor INP.",[20,1032,1033],{},"Understanding which metric is failing on your specific page is the prerequisite to fixing it. Throwing generic optimization advice at a poor INP score won't help if the root cause is a large contentful paint that's timing out.",[40,1035],{},[15,1037,1039],{"id":1038},"the-performance-measurement-stack","The Performance Measurement Stack",[20,1041,1042,1045],{},[26,1043,1044],{},"Lighthouse:"," Run it in Chrome DevTools (incognito window, simulated throttling) for on-demand lab measurement. Lighthouse gives you a simulated user on a \"Moto G4-equivalent\" device with a slow 4G connection — which is intentionally conservative. Passing Lighthouse in lab conditions is necessary but not sufficient.",[20,1047,1048,1051],{},[26,1049,1050],{},"PageSpeed Insights:"," Combines Lighthouse lab data with Chrome User Experience Report (CrUX) field data for your URL. The field data is what Google actually uses for ranking. If lab and field diverge significantly, investigate why — sometimes the page behaves differently in lab conditions than in real-world browser diversity.",[20,1053,1054,1057],{},[26,1055,1056],{},"Google Search Console (Core Web Vitals tab):"," Shows URL-level field data directly from Google's real user measurements. This is the authoritative source for your search ranking health.",[20,1059,1060],{},[26,1061,1062],{},"web-vitals library in production:",[57,1064,1068],{"className":1065,"code":1066,"language":1067,"meta":62,"style":62},"language-javascript shiki shiki-themes github-dark","import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'\n\nFunction sendMetric({ name, value, id }) {\n // Send to your analytics\n fetch('/api/vitals', {\n method: 'POST',\n body: JSON.stringify({ name, value, id, url: location.href })\n })\n}\n\nOnLCP(sendMetric)\nonINP(sendMetric)\nonCLS(sendMetric)\nonFCP(sendMetric)\nonTTFB(sendMetric)\n","javascript",[47,1069,1070,1084,1088,1099,1104,1117,1128,1145,1150,1154,1158,1166,1173,1181,1189],{"__ignoreMap":62},[66,1071,1072,1075,1078,1081],{"class":68,"line":69},[66,1073,1074],{"class":72},"import",[66,1076,1077],{"class":76}," { onLCP, onINP, onCLS, onFCP, onTTFB } ",[66,1079,1080],{"class":72},"from",[66,1082,1083],{"class":90}," 'web-vitals'\n",[66,1085,1086],{"class":68,"line":80},[66,1087,459],{"emptyLinePlaceholder":458},[66,1089,1090,1093,1096],{"class":68,"line":97},[66,1091,1092],{"class":76},"Function ",[66,1094,1095],{"class":219},"sendMetric",[66,1097,1098],{"class":76},"({ name, value, id }) {\n",[66,1100,1101],{"class":68,"line":128},[66,1102,1103],{"class":582}," // Send to your analytics\n",[66,1105,1106,1109,1111,1114],{"class":68,"line":141},[66,1107,1108],{"class":219}," fetch",[66,1110,108],{"class":76},[66,1112,1113],{"class":90},"'/api/vitals'",[66,1115,1116],{"class":76},", {\n",[66,1118,1119,1122,1125],{"class":68,"line":259},[66,1120,1121],{"class":76}," method: ",[66,1123,1124],{"class":90},"'POST'",[66,1126,1127],{"class":76},",\n",[66,1129,1130,1133,1136,1139,1142],{"class":68,"line":265},[66,1131,1132],{"class":76}," body: ",[66,1134,1135],{"class":83},"JSON",[66,1137,1138],{"class":76},".",[66,1140,1141],{"class":219},"stringify",[66,1143,1144],{"class":76},"({ name, value, id, url: location.href })\n",[66,1146,1147],{"class":68,"line":450},[66,1148,1149],{"class":76}," })\n",[66,1151,1152],{"class":68,"line":455},[66,1153,144],{"class":76},[66,1155,1156],{"class":68,"line":462},[66,1157,459],{"emptyLinePlaceholder":458},[66,1159,1160,1163],{"class":68,"line":470},[66,1161,1162],{"class":219},"OnLCP",[66,1164,1165],{"class":76},"(sendMetric)\n",[66,1167,1168,1171],{"class":68,"line":491},[66,1169,1170],{"class":219},"onINP",[66,1172,1165],{"class":76},[66,1174,1176,1179],{"class":68,"line":1175},13,[66,1177,1178],{"class":219},"onCLS",[66,1180,1165],{"class":76},[66,1182,1184,1187],{"class":68,"line":1183},14,[66,1185,1186],{"class":219},"onFCP",[66,1188,1165],{"class":76},[66,1190,1192,1195],{"class":68,"line":1191},15,[66,1193,1194],{"class":219},"onTTFB",[66,1196,1165],{"class":76},[20,1198,1199],{},"This gives you field data from your actual users, segmented by URL, page type, device type, and connection speed. It's the most honest picture of your performance.",[40,1201],{},[15,1203,1205],{"id":1204},"ttfb-the-foundation","TTFB: The Foundation",[20,1207,1208],{},"Everything starts with TTFB. If your server takes 2 seconds to start responding, LCP can't be under 2.5 seconds regardless of how well everything else is optimized.",[20,1210,1211,1214],{},[26,1212,1213],{},"Diagnose TTFB:"," In Chrome DevTools Network tab, click on the HTML document request and look at the \"Timing\" tab. Time to First Byte is shown explicitly.",[20,1216,1217],{},[26,1218,1219],{},"Common TTFB causes and fixes:",[20,1221,1222],{},"Slow database queries in the server-side render path. For SSR pages, the server must query the database before it can send the first byte. A slow query = high TTFB. Fix: query optimization, caching, or streaming HTML (send the shell immediately, stream the data).",[20,1224,1225],{},"No caching on the edge. If every HTML request goes to your origin server, you're paying the origin's response time for every request. A CDN that caches HTML at the edge (or serves stale-while-revalidate) can get TTFB under 100ms for cached URLs.",[20,1227,1228],{},"No HTTP/2 or HTTP/3. Older protocols have more connection overhead. Most modern infrastructure supports HTTP/2; enable it if you haven't.",[40,1230],{},[15,1232,1234],{"id":1233},"lcp-optimization-checklist","LCP Optimization Checklist",[20,1236,1237],{},"Work through these in order — each one can have a significant impact:",[1239,1240,1241,1254,1267,1273,1279],"ol",{},[307,1242,1243,1246,1247,1250,1251,1138],{},[26,1244,1245],{},"Identify your LCP element."," In Chrome DevTools, enable the \"Web Vitals\" checkbox in the Performance panel, run a recording, and find the LCP mark. Or use ",[47,1248,1249],{},"PerformanceObserver"," to log it: ",[47,1252,1253],{},"new PerformanceObserver((list) => console.log(list.getEntries())).observe({ type: 'largest-contentful-paint', buffered: true })",[307,1255,1256,1259,1260,1263,1264,1138],{},[26,1257,1258],{},"Preload the LCP image."," If LCP is an image, add ",[47,1261,1262],{},"\u003Clink rel=\"preload\" as=\"image\" href=\"...\" fetchpriority=\"high\">"," to the ",[47,1265,1266],{},"\u003Chead>",[307,1268,1269,1272],{},[26,1270,1271],{},"Ensure the LCP element is in the initial HTML."," If LCP is rendered client-side (React/Vue hydration), the HTML document won't contain the element, and LCP is delayed until JavaScript executes. Use SSR or SSG.",[307,1274,1275,1278],{},[26,1276,1277],{},"Check image format and size."," LCP images should be WebP or AVIF, properly sized for the display size (not 4000px wide for a 1200px container), and served from a CDN.",[307,1280,1281,1284,1285,1288,1289,1292,1293,1296,1297,1299],{},[26,1282,1283],{},"Eliminate render-blocking scripts."," Any ",[47,1286,1287],{},"\u003Cscript>"," without ",[47,1290,1291],{},"async"," or ",[47,1294,1295],{},"defer"," in the ",[47,1298,1266],{}," blocks rendering. Audit your script tags.",[40,1301],{},[15,1303,1305],{"id":1304},"inp-optimization-checklist","INP Optimization Checklist",[20,1307,1308],{},"INP problems are almost always long tasks on the main thread. The diagnostic process:",[1239,1310,1311,1316,1322,1328,1334,1347,1353],{},[307,1312,1313],{},[26,1314,1315],{},"Open Chrome DevTools → Performance tab → enable INP recording.",[307,1317,1318,1321],{},[26,1319,1320],{},"Click around the page normally"," for 30 seconds while recording.",[307,1323,1324,1327],{},[26,1325,1326],{},"Find high INP interactions"," in the recording. Click on each to see the main thread activity during that interaction.",[307,1329,1330,1333],{},[26,1331,1332],{},"Identify the long task."," What JavaScript is running for more than 50ms during the interaction? Is it in your code, a third-party script, or a framework operation?",[307,1335,1336,1339,1340,1292,1343,1346],{},[26,1337,1338],{},"Break up long tasks."," Use ",[47,1341,1342],{},"setTimeout(0)",[47,1344,1345],{},"scheduler.yield()"," to yield to the browser between expensive operations.",[307,1348,1349,1352],{},[26,1350,1351],{},"Move work off the main thread."," For computationally intensive work (data processing, complex calculations), use a Web Worker.",[307,1354,1355,1358],{},[26,1356,1357],{},"Audit third-party scripts."," Google Tag Manager, chat widgets, ad scripts — these run on your main thread and can cause poor INP. Test the page with all third-party scripts disabled to see the baseline.",[40,1360],{},[15,1362,1364],{"id":1363},"cls-optimization-checklist","CLS Optimization Checklist",[1239,1366,1367,1385,1391,1405,1411],{},[307,1368,1369,1372,1373,1376,1377,1380,1381,1384],{},[26,1370,1371],{},"Set explicit dimensions on all images:"," ",[47,1374,1375],{},"width"," and ",[47,1378,1379],{},"height"," HTML attributes, or ",[47,1382,1383],{},"aspect-ratio"," in CSS.",[307,1386,1387,1390],{},[26,1388,1389],{},"Reserve space for dynamic content:"," Cookie banners, notification bars, embedded ads — add a container with explicit height before they load.",[307,1392,1393,1339,1396,479,1398,479,1401,1404],{},[26,1394,1395],{},"Match fallback font metrics to web fonts:",[47,1397,619],{},[47,1399,1400],{},"ascent-override",[47,1402,1403],{},"descent-override"," to prevent layout shift from font swaps.",[307,1406,1407,1410],{},[26,1408,1409],{},"Avoid inserting content above existing content"," in response to user interactions (unless the user explicitly requests it).",[307,1412,1413,1416],{},[26,1414,1415],{},"Transition animations:"," If you're showing/hiding content, use CSS animations that transform opacity and transform (GPU-accelerated) rather than height and margin (which trigger layout).",[40,1418],{},[15,1420,1422],{"id":1421},"the-performance-budget","The Performance Budget",[20,1424,1425],{},"A performance budget is a constraint on your performance metrics that you enforce in CI. Common budgets:",[304,1427,1428,1431,1434,1437],{},[307,1429,1430],{},"Total JavaScript: under 150KB (gzipped)",[307,1432,1433],{},"LCP: under 2.5 seconds on simulated slow 4G",[307,1435,1436],{},"Lighthouse performance score: over 90",[307,1438,1439],{},"No new long tasks over 300ms",[20,1441,1442],{},"Tools like Lighthouse CI, Bundlesize, and webpack's built-in size limits can enforce these budgets in your CI pipeline, failing builds that introduce regressions.",[40,1444],{},[20,1446,1447],{},"Frontend performance is an ongoing discipline, not a project. The optimizations you make today may be reversed by a new dependency or an unoptimized image six months from now. Build the measurement infrastructure, set the budgets, enforce them in CI, and treat regressions as bugs.",[20,1449,1450,1451,1138],{},"If you're working on a site with poor Web Vitals and want a systematic audit and prioritized fix plan, book a call at ",[690,1452,695],{"href":692,"rel":1453},[694],[40,1455],{},[15,1457,702],{"id":701},[304,1459,1460,1464,1468,1472],{},[307,1461,1462],{},[690,1463,710],{"href":709},[307,1465,1466],{},[690,1467,7],{"href":755},[307,1469,1470],{},[690,1471,728],{"href":727},[307,1473,1474],{},[690,1475,1477],{"href":1476},"/blog/database-query-performance","Database Query Performance: Finding and Fixing the Slow Ones",[730,1479,1480],{},"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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":62,"searchDepth":97,"depth":97,"links":1482},[1483,1484,1485,1486,1487,1488,1489,1490],{"id":993,"depth":80,"text":994},{"id":1038,"depth":80,"text":1039},{"id":1204,"depth":80,"text":1205},{"id":1233,"depth":80,"text":1234},{"id":1304,"depth":80,"text":1305},{"id":1363,"depth":80,"text":1364},{"id":1421,"depth":80,"text":1422},{"id":701,"depth":80,"text":702},"Frontend performance is not just about speed — it's about the specific metrics that affect user experience and search rankings. Here's how to measure, diagnose, and hit your targets.",[1493,1494],"frontend performance","web performance optimization",{},{"title":722,"description":1491},"blog/frontend-performance-guide",[759,1499,761],"Frontend","FLkZPo5PAPjkJGQ3GcQ5e_G03mA7EuhaDm-6Bnlud7g",{"id":1502,"title":1503,"author":1504,"body":1505,"category":1673,"date":746,"description":1674,"extension":748,"featured":749,"image":750,"keywords":1675,"meta":1678,"navigation":458,"path":1679,"readTime":455,"seo":1680,"stem":1681,"tags":1682,"__hash__":1686},"blog/blog/future-of-software-development-ai.md","The Future of Software Development in an AI World",{"name":9,"bio":10},{"type":12,"value":1506,"toc":1663},[1507,1511,1514,1517,1519,1523,1526,1529,1532,1535,1537,1541,1544,1547,1550,1553,1555,1559,1562,1565,1568,1571,1573,1577,1580,1583,1586,1589,1591,1595,1598,1601,1604,1607,1609,1613,1616,1619,1622,1625,1633,1635,1637],[15,1508,1510],{"id":1509},"predictions-are-hard-patterns-are-more-reliable","Predictions Are Hard. Patterns Are More Reliable.",[20,1512,1513],{},"I'm skeptical of \"future of software development\" articles that read as science fiction dressed in business language. Nobody accurately predicted that AI code generation would reach its current capability level two years ago; nobody should be claiming high-confidence predictions about what software development looks like in 2030.",[20,1515,1516],{},"What I can offer is a pattern-based view: here are the directions I can observe as clear trends from where we are now, here is what the evidence suggests about how they develop, and here is my best thinking about the implications. I'll be wrong about specifics. I'm reasonably confident about the direction.",[40,1518],{},[15,1520,1522],{"id":1521},"the-shift-thats-already-happening-specification-over-implementation","The Shift That's Already Happening: Specification Over Implementation",[20,1524,1525],{},"The ratio of human effort in software development is shifting toward specification (defining what the software should do and how it should be designed) and away from implementation (writing the code that does it).",[20,1527,1528],{},"This is already my experience. In my practice today, a larger share of my working time goes into: understanding client requirements, designing architecture, reviewing AI-generated output for correctness and quality, and refining specifications when the output reveals ambiguities in the requirements. A smaller share of my time goes into writing code from scratch.",[20,1530,1531],{},"This shift will continue. As AI tools get better at implementation — writing code correctly from well-specified requirements — the human role becomes more concentrated in the phases that require context, judgment, and communication: understanding what needs to be built, designing how it should be structured, validating that what was built is correct.",[20,1533,1534],{},"The implication for developers: the skills that matter most are shifting. Deep expertise in what correct software behavior looks like — in your domain, for your users, given your constraints — becomes more valuable. Mechanical coding proficiency becomes less differentiating. The developers who will struggle are those whose primary value was knowing how to write code without having developed strong judgment about what code should do.",[40,1536],{},[15,1538,1540],{"id":1539},"agentic-systems-will-handle-more-of-the-development-workflow","Agentic Systems Will Handle More of the Development Workflow",[20,1542,1543],{},"Today's AI coding tools are primarily interactive: you ask, they respond, you evaluate, you ask again. The loop is human-initiated at each step.",[20,1545,1546],{},"The trend is toward more autonomous operation: agentic systems that receive a specification, break it into tasks, implement those tasks, run tests, evaluate the results, iterate on failures, and produce a complete implementation for human review.",[20,1548,1549],{},"This is already partially possible with current tools. It will become more capable and more reliable. The end state I'm projecting: for well-defined features in established codebases with good test coverage, the human role in implementation will be: write a clear specification, review the agent's implementation, approve or refine.",[20,1551,1552],{},"This doesn't mean \"AI will write all the software.\" It means that the software development process will increasingly resemble the relationship between an experienced architect and a fast, capable development team: the architect specifies, designs, and reviews; the team implements; the architect validates. Except that \"the team\" is increasingly automated.",[40,1554],{},[15,1556,1558],{"id":1557},"the-rising-value-of-domain-expertise-in-software-development","The Rising Value of Domain Expertise in Software Development",[20,1560,1561],{},"As the implementation layer becomes more automated, domain expertise — understanding the problem space deeply — becomes the differentiating capability.",[20,1563,1564],{},"A developer who deeply understands healthcare operations and builds software for healthcare clients is more valuable than one who knows TypeScript better. A developer who has built financial systems and understands the regulatory landscape is more valuable than one who's studied more algorithms. The domain expertise was always valuable; it becomes more relatively important as implementation becomes less scarce.",[20,1566,1567],{},"This is a good development for software practitioners who have invested in building deep domain expertise alongside their technical skills. It's challenging for those whose identity as a developer was primarily technical — who want to be experts in how to build things rather than what should be built.",[20,1569,1570],{},"The advice I'd give to anyone in software development early in their career: invest seriously in becoming expert in the domains where you work. If you build software for restaurants, understand the restaurant business deeply. If you build fintech, understand finance deeply. The technical skills remain necessary but become table stakes faster than the domain expertise does.",[40,1572],{},[15,1574,1576],{"id":1575},"the-software-stack-will-become-more-opinionated-and-conventional","The Software Stack Will Become More Opinionated and Conventional",[20,1578,1579],{},"Here's a less-obvious prediction: software development will become more standardized, not less, as AI tools mature.",[20,1581,1582],{},"The reason: AI coding tools are most effective in conventional codebases that follow established patterns. Custom, idiosyncratic architectures that a developer designed based on unique constraints are harder for AI tools to work in effectively. The incentive structure shifts toward following widely-used patterns that AI tools know well.",[20,1584,1585],{},"This is already visible in the frameworks and conventions that AI tools handle best. Well-documented, widely-adopted frameworks (Next.js, FastAPI, Rails, etc.) get better AI assistance than obscure or custom frameworks. The feedback loop: developers using conventional stacks get more AI leverage, which makes conventional stacks more attractive, which concentrates more developer usage in fewer conventions, which makes AI tools better at those conventions.",[20,1587,1588],{},"The practical implication: the value of inventing novel architectures will be questioned more often. \"Why are we building this custom thing when a conventional approach would work and give us more AI tooling leverage?\" is a legitimate question that will be asked more.",[40,1590],{},[15,1592,1594],{"id":1593},"quality-standards-will-rise","Quality Standards Will Rise",[20,1596,1597],{},"This is counterintuitive to some: as AI tools make code generation faster and cheaper, I expect average code quality to increase, not decrease.",[20,1599,1600],{},"The reason: AI tools are good at certain quality dimensions. They produce well-structured code, catch common bugs, enforce consistent style. The output of AI-assisted development is often more structurally clean than the output of a rushed developer writing under time pressure.",[20,1602,1603],{},"Additionally: when implementation is faster, there is more time for quality work — code review, testing, refactoring. The constraint that limited quality investment was time; reduce the time cost of implementation and some of that time goes into quality.",[20,1605,1606],{},"The caveat: this is a trend that requires deliberate choice. The organizations that will see rising quality are those that invest the time saved by AI tools into quality work. Organizations that simply increase output expectations proportionally with AI productivity gains — that treat faster code generation as an excuse to demand proportionally more output — won't see quality improvements.",[40,1608],{},[15,1610,1612],{"id":1611},"software-development-becomes-more-accessible","Software Development Becomes More Accessible",[20,1614,1615],{},"More people will build software in the coming years. Not because AI eliminates the need for expertise, but because the entry point moves. A business owner who wants to build a custom internal tool, a researcher who needs custom data analysis software, a consultant who wants to automate their workflow — these people can increasingly accomplish what they're trying to do with AI assistance at a level of capability that previously required hiring a developer.",[20,1617,1618],{},"This is positive. More software gets built, more problems get solved, and the scope of who participates in building software expands.",[20,1620,1621],{},"It also means that the professional developer's value proposition shifts more clearly toward: complex systems, critical systems, systems where the cost of getting it wrong is high, and systems that require the architectural thinking and domain expertise that remains genuinely human-level work.",[20,1623,1624],{},"I am not worried about the future of software development as a profession. I am attentive to the shifts in what matters within that profession. The developers who understand those shifts and position accordingly will find the next decade the most interesting and productive of their careers.",[20,1626,1627,1628,1632],{},"If you're building software for your business and want to work with a practitioner who thinks carefully about these trends and applies them to real work, ",[690,1629,1631],{"href":692,"rel":1630},[694],"schedule a conversation at Calendly",". I build AI-native applications for businesses that want to be ahead of where software development is going, not catching up to where it's been.",[40,1634],{},[15,1636,702],{"id":701},[304,1638,1639,1645,1651,1657],{},[307,1640,1641],{},[690,1642,1644],{"href":1643},"/blog/ai-software-development-trends-2026","AI Software Development Trends for 2026: A Practitioner's View",[307,1646,1647],{},[690,1648,1650],{"href":1649},"/blog/agentic-ai-software-development","Agentic AI Software Development: What It Is and Why It Changes Everything",[307,1652,1653],{},[690,1654,1656],{"href":1655},"/blog/ai-documentation-generation","AI-Generated Documentation: What It Can and Can't Replace",[307,1658,1659],{},[690,1660,1662],{"href":1661},"/blog/building-ai-native-applications","Building AI-Native Applications: Architecture Patterns That Actually Work",{"title":62,"searchDepth":97,"depth":97,"links":1664},[1665,1666,1667,1668,1669,1670,1671,1672],{"id":1509,"depth":80,"text":1510},{"id":1521,"depth":80,"text":1522},{"id":1539,"depth":80,"text":1540},{"id":1557,"depth":80,"text":1558},{"id":1575,"depth":80,"text":1576},{"id":1593,"depth":80,"text":1594},{"id":1611,"depth":80,"text":1612},{"id":701,"depth":80,"text":702},"AI","A practicing architect's perspective on where software development is actually headed — what AI changes structurally about how software gets built, and what that means for businesses and developers.",[1676,1677],"future of software development","AI software development",{},"/blog/future-of-software-development-ai",{"title":1503,"description":1674},"blog/future-of-software-development-ai",[1683,1673,983,1684,1685],"Future of Software","Technology","Career","HGYMOwy0gXb5ktESyLuPaCXjPs98vAqiWaq-u9LpAkc",{"id":1688,"title":1689,"author":1690,"body":1691,"category":2835,"date":746,"description":2836,"extension":748,"featured":749,"image":750,"keywords":2837,"meta":2840,"navigation":458,"path":2841,"readTime":265,"seo":2842,"stem":2843,"tags":2844,"__hash__":2848},"blog/blog/github-actions-cicd-guide.md","GitHub Actions CI/CD: A Complete Setup Guide for Modern Projects",{"name":9,"bio":10},{"type":12,"value":1692,"toc":2821},[1693,1697,1700,1703,1707,1710,1715,1722,2047,2066,2069,2073,2079,2302,2308,2312,2315,2322,2329,2333,2344,2397,2407,2411,2414,2516,2519,2523,2530,2537,2603,2606,2658,2661,2665,2668,2702,2705,2709,2723,2726,2729,2760,2763,2767,2777,2780,2782,2788,2790,2792,2818],[1694,1695,1689],"h1",{"id":1696},"github-actions-cicd-a-complete-setup-guide-for-modern-projects",[20,1698,1699],{},"Continuous integration is one of those practices that sounds obvious until you work on a team that does not do it. I have been brought into enough legacy projects — codebases where deployments are manual SSH sessions, where nobody knows what is actually running in production, where \"we'll test it on staging\" is the quality gate — to know exactly what it costs. Bugs shipped faster, rollbacks handled manually, developers afraid to merge.",[20,1701,1702],{},"GitHub Actions changed the calculus for small and mid-sized teams. You do not need a dedicated DevOps engineer to run a solid CI/CD pipeline. You need a YAML file and some discipline. Here is how I set it up on every project.",[15,1704,1706],{"id":1705},"the-two-pipelines-you-need","The Two Pipelines You Need",[20,1708,1709],{},"Most projects need exactly two workflows: one that runs on every pull request, and one that deploys when you merge to main. The PR workflow is your quality gate. The deploy workflow is your delivery mechanism. Keep them separate.",[1711,1712,1714],"h3",{"id":1713},"the-ci-workflow-pull-request","The CI Workflow (Pull Request)",[20,1716,1717,1718,1721],{},"This runs on every PR and on every push to ",[47,1719,1720],{},"main",". It must be fast — under five minutes ideally — or developers start skipping it mentally.",[57,1723,1727],{"className":1724,"code":1725,"language":1726,"meta":62,"style":62},"language-yaml shiki shiki-themes github-dark","name: CI\n\nOn:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\nJobs:\n test:\n runs-on: ubuntu-latest\n\n services:\n postgres:\n image: postgres:16-alpine\n env:\n POSTGRES_PASSWORD: testpassword\n POSTGRES_DB: testdb\n options: >-\n --health-cmd pg_isready\n --health-interval 10s\n --health-timeout 5s\n --health-retries 5\n ports:\n - 5432:5432\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: npm\n\n - run: npm ci\n\n - name: Run type check\n run: npm run typecheck\n\n - name: Run linter\n run: npm run lint\n\n - name: Run tests\n run: npm run test\n env:\n DATABASE_URL: postgres://postgres:testpassword@localhost:5432/testdb\n NODE_ENV: test\n","yaml",[47,1728,1729,1739,1743,1751,1758,1771,1778,1788,1792,1799,1806,1816,1820,1827,1834,1844,1852,1863,1874,1885,1891,1897,1903,1909,1915,1921,1926,1932,1938,1943,1949,1955,1961,1967,1972,1978,1983,1989,1995,2000,2006,2012,2017,2023,2029,2035,2041],{"__ignoreMap":62},[66,1730,1731,1734,1736],{"class":68,"line":69},[66,1732,1733],{"class":213},"name",[66,1735,87],{"class":76},[66,1737,1738],{"class":90},"CI\n",[66,1740,1741],{"class":68,"line":80},[66,1742,459],{"emptyLinePlaceholder":458},[66,1744,1745,1748],{"class":68,"line":97},[66,1746,1747],{"class":83},"On",[66,1749,1750],{"class":76},":\n",[66,1752,1753,1756],{"class":68,"line":128},[66,1754,1755],{"class":213}," push",[66,1757,1750],{"class":76},[66,1759,1760,1763,1766,1768],{"class":68,"line":141},[66,1761,1762],{"class":213}," branches",[66,1764,1765],{"class":76},": [",[66,1767,1720],{"class":90},[66,1769,1770],{"class":76},"]\n",[66,1772,1773,1776],{"class":68,"line":259},[66,1774,1775],{"class":213}," pull_request",[66,1777,1750],{"class":76},[66,1779,1780,1782,1784,1786],{"class":68,"line":265},[66,1781,1762],{"class":213},[66,1783,1765],{"class":76},[66,1785,1720],{"class":90},[66,1787,1770],{"class":76},[66,1789,1790],{"class":68,"line":450},[66,1791,459],{"emptyLinePlaceholder":458},[66,1793,1794,1797],{"class":68,"line":455},[66,1795,1796],{"class":213},"Jobs",[66,1798,1750],{"class":76},[66,1800,1801,1804],{"class":68,"line":462},[66,1802,1803],{"class":213}," test",[66,1805,1750],{"class":76},[66,1807,1808,1811,1813],{"class":68,"line":470},[66,1809,1810],{"class":213}," runs-on",[66,1812,87],{"class":76},[66,1814,1815],{"class":90},"ubuntu-latest\n",[66,1817,1818],{"class":68,"line":491},[66,1819,459],{"emptyLinePlaceholder":458},[66,1821,1822,1825],{"class":68,"line":1175},[66,1823,1824],{"class":213}," services",[66,1826,1750],{"class":76},[66,1828,1829,1832],{"class":68,"line":1183},[66,1830,1831],{"class":213}," postgres",[66,1833,1750],{"class":76},[66,1835,1836,1839,1841],{"class":68,"line":1191},[66,1837,1838],{"class":213}," image",[66,1840,87],{"class":76},[66,1842,1843],{"class":90},"postgres:16-alpine\n",[66,1845,1847,1850],{"class":68,"line":1846},16,[66,1848,1849],{"class":213}," env",[66,1851,1750],{"class":76},[66,1853,1855,1858,1860],{"class":68,"line":1854},17,[66,1856,1857],{"class":213}," POSTGRES_PASSWORD",[66,1859,87],{"class":76},[66,1861,1862],{"class":90},"testpassword\n",[66,1864,1866,1869,1871],{"class":68,"line":1865},18,[66,1867,1868],{"class":213}," POSTGRES_DB",[66,1870,87],{"class":76},[66,1872,1873],{"class":90},"testdb\n",[66,1875,1877,1880,1882],{"class":68,"line":1876},19,[66,1878,1879],{"class":213}," options",[66,1881,87],{"class":76},[66,1883,1884],{"class":72},">-\n",[66,1886,1888],{"class":68,"line":1887},20,[66,1889,1890],{"class":90}," --health-cmd pg_isready\n",[66,1892,1894],{"class":68,"line":1893},21,[66,1895,1896],{"class":90}," --health-interval 10s\n",[66,1898,1900],{"class":68,"line":1899},22,[66,1901,1902],{"class":90}," --health-timeout 5s\n",[66,1904,1906],{"class":68,"line":1905},23,[66,1907,1908],{"class":90}," --health-retries 5\n",[66,1910,1912],{"class":68,"line":1911},24,[66,1913,1914],{"class":90}," ports:\n",[66,1916,1918],{"class":68,"line":1917},25,[66,1919,1920],{"class":90}," - 5432:5432\n",[66,1922,1924],{"class":68,"line":1923},26,[66,1925,459],{"emptyLinePlaceholder":458},[66,1927,1929],{"class":68,"line":1928},27,[66,1930,1931],{"class":90}," steps:\n",[66,1933,1935],{"class":68,"line":1934},28,[66,1936,1937],{"class":90}," - uses: actions/checkout@v4\n",[66,1939,1941],{"class":68,"line":1940},29,[66,1942,459],{"emptyLinePlaceholder":458},[66,1944,1946],{"class":68,"line":1945},30,[66,1947,1948],{"class":90}," - uses: actions/setup-node@v4\n",[66,1950,1952],{"class":68,"line":1951},31,[66,1953,1954],{"class":90}," with:\n",[66,1956,1958],{"class":68,"line":1957},32,[66,1959,1960],{"class":90}," node-version: 20\n",[66,1962,1964],{"class":68,"line":1963},33,[66,1965,1966],{"class":90}," cache: npm\n",[66,1968,1970],{"class":68,"line":1969},34,[66,1971,459],{"emptyLinePlaceholder":458},[66,1973,1975],{"class":68,"line":1974},35,[66,1976,1977],{"class":90}," - run: npm ci\n",[66,1979,1981],{"class":68,"line":1980},36,[66,1982,459],{"emptyLinePlaceholder":458},[66,1984,1986],{"class":68,"line":1985},37,[66,1987,1988],{"class":90}," - name: Run type check\n",[66,1990,1992],{"class":68,"line":1991},38,[66,1993,1994],{"class":90}," run: npm run typecheck\n",[66,1996,1998],{"class":68,"line":1997},39,[66,1999,459],{"emptyLinePlaceholder":458},[66,2001,2003],{"class":68,"line":2002},40,[66,2004,2005],{"class":90}," - name: Run linter\n",[66,2007,2009],{"class":68,"line":2008},41,[66,2010,2011],{"class":90}," run: npm run lint\n",[66,2013,2015],{"class":68,"line":2014},42,[66,2016,459],{"emptyLinePlaceholder":458},[66,2018,2020],{"class":68,"line":2019},43,[66,2021,2022],{"class":90}," - name: Run tests\n",[66,2024,2026],{"class":68,"line":2025},44,[66,2027,2028],{"class":90}," run: npm run test\n",[66,2030,2032],{"class":68,"line":2031},45,[66,2033,2034],{"class":90}," env:\n",[66,2036,2038],{"class":68,"line":2037},46,[66,2039,2040],{"class":90}," DATABASE_URL: postgres://postgres:testpassword@localhost:5432/testdb\n",[66,2042,2044],{"class":68,"line":2043},47,[66,2045,2046],{"class":90}," NODE_ENV: test\n",[20,2048,2049,2050,2053,2054,2057,2058,2061,2062,2065],{},"A few things to notice. The ",[47,2051,2052],{},"actions/setup-node@v4"," with ",[47,2055,2056],{},"cache: npm"," automatically caches your ",[47,2059,2060],{},"node_modules"," between runs. This alone can cut your CI time by two minutes. The ",[47,2063,2064],{},"services"," block spins up a real PostgreSQL instance for integration tests. Testing against a real database catches a class of bugs that mocking never will.",[20,2067,2068],{},"Type checking and linting run as separate steps, not bundled with tests. This gives you precise failure messages. When CI fails, you want to know immediately whether it was a type error, a lint violation, or a broken test.",[1711,2070,2072],{"id":2071},"the-deploy-workflow","The Deploy Workflow",[20,2074,2075,2076,2078],{},"This runs only when a push lands on ",[47,2077,1720],{}," — typically via a merged PR.",[57,2080,2082],{"className":1724,"code":2081,"language":1726,"meta":62,"style":62},"name: Deploy\n\nOn:\n push:\n branches: [main]\n\nJobs:\n deploy:\n runs-on: ubuntu-latest\n needs: [] # Reference your test job if in same file\n environment: production\n\n steps:\n - uses: actions/checkout@v4\n\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n cache: npm\n\n - run: npm ci\n - run: npm run build\n\n - name: Deploy to production\n run: |\n # Your deployment command here\n # e.g., fly deploy, railway up, docker push + ssh\n env:\n DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}\n",[47,2083,2084,2093,2097,2103,2109,2119,2123,2129,2136,2144,2155,2165,2169,2176,2189,2193,2204,2211,2221,2231,2235,2247,2258,2262,2273,2283,2288,2293,2297],{"__ignoreMap":62},[66,2085,2086,2088,2090],{"class":68,"line":69},[66,2087,1733],{"class":213},[66,2089,87],{"class":76},[66,2091,2092],{"class":90},"Deploy\n",[66,2094,2095],{"class":68,"line":80},[66,2096,459],{"emptyLinePlaceholder":458},[66,2098,2099,2101],{"class":68,"line":97},[66,2100,1747],{"class":83},[66,2102,1750],{"class":76},[66,2104,2105,2107],{"class":68,"line":128},[66,2106,1755],{"class":213},[66,2108,1750],{"class":76},[66,2110,2111,2113,2115,2117],{"class":68,"line":141},[66,2112,1762],{"class":213},[66,2114,1765],{"class":76},[66,2116,1720],{"class":90},[66,2118,1770],{"class":76},[66,2120,2121],{"class":68,"line":259},[66,2122,459],{"emptyLinePlaceholder":458},[66,2124,2125,2127],{"class":68,"line":265},[66,2126,1796],{"class":213},[66,2128,1750],{"class":76},[66,2130,2131,2134],{"class":68,"line":450},[66,2132,2133],{"class":213}," deploy",[66,2135,1750],{"class":76},[66,2137,2138,2140,2142],{"class":68,"line":455},[66,2139,1810],{"class":213},[66,2141,87],{"class":76},[66,2143,1815],{"class":90},[66,2145,2146,2149,2152],{"class":68,"line":462},[66,2147,2148],{"class":213}," needs",[66,2150,2151],{"class":76},": [] ",[66,2153,2154],{"class":582},"# Reference your test job if in same file\n",[66,2156,2157,2160,2162],{"class":68,"line":470},[66,2158,2159],{"class":213}," environment",[66,2161,87],{"class":76},[66,2163,2164],{"class":90},"production\n",[66,2166,2167],{"class":68,"line":491},[66,2168,459],{"emptyLinePlaceholder":458},[66,2170,2171,2174],{"class":68,"line":1175},[66,2172,2173],{"class":213}," steps",[66,2175,1750],{"class":76},[66,2177,2178,2181,2184,2186],{"class":68,"line":1183},[66,2179,2180],{"class":76}," - ",[66,2182,2183],{"class":213},"uses",[66,2185,87],{"class":76},[66,2187,2188],{"class":90},"actions/checkout@v4\n",[66,2190,2191],{"class":68,"line":1191},[66,2192,459],{"emptyLinePlaceholder":458},[66,2194,2195,2197,2199,2201],{"class":68,"line":1846},[66,2196,2180],{"class":76},[66,2198,2183],{"class":213},[66,2200,87],{"class":76},[66,2202,2203],{"class":90},"actions/setup-node@v4\n",[66,2205,2206,2209],{"class":68,"line":1854},[66,2207,2208],{"class":213}," with",[66,2210,1750],{"class":76},[66,2212,2213,2216,2218],{"class":68,"line":1865},[66,2214,2215],{"class":213}," node-version",[66,2217,87],{"class":76},[66,2219,2220],{"class":83},"20\n",[66,2222,2223,2226,2228],{"class":68,"line":1876},[66,2224,2225],{"class":213}," cache",[66,2227,87],{"class":76},[66,2229,2230],{"class":90},"npm\n",[66,2232,2233],{"class":68,"line":1887},[66,2234,459],{"emptyLinePlaceholder":458},[66,2236,2237,2239,2242,2244],{"class":68,"line":1893},[66,2238,2180],{"class":76},[66,2240,2241],{"class":213},"run",[66,2243,87],{"class":76},[66,2245,2246],{"class":90},"npm ci\n",[66,2248,2249,2251,2253,2255],{"class":68,"line":1899},[66,2250,2180],{"class":76},[66,2252,2241],{"class":213},[66,2254,87],{"class":76},[66,2256,2257],{"class":90},"npm run build\n",[66,2259,2260],{"class":68,"line":1905},[66,2261,459],{"emptyLinePlaceholder":458},[66,2263,2264,2266,2268,2270],{"class":68,"line":1911},[66,2265,2180],{"class":76},[66,2267,1733],{"class":213},[66,2269,87],{"class":76},[66,2271,2272],{"class":90},"Deploy to production\n",[66,2274,2275,2278,2280],{"class":68,"line":1917},[66,2276,2277],{"class":213}," run",[66,2279,87],{"class":76},[66,2281,2282],{"class":72},"|\n",[66,2284,2285],{"class":68,"line":1923},[66,2286,2287],{"class":90}," # Your deployment command here\n",[66,2289,2290],{"class":68,"line":1928},[66,2291,2292],{"class":90}," # e.g., fly deploy, railway up, docker push + ssh\n",[66,2294,2295],{"class":68,"line":1934},[66,2296,2034],{"class":90},[66,2298,2299],{"class":68,"line":1940},[66,2300,2301],{"class":90}," DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}\n",[20,2303,45,2304,2307],{},[47,2305,2306],{},"environment: production"," key is important. GitHub Environments let you require manual approval before deploying, restrict which branches can deploy, and store environment-specific secrets separately from repository secrets. Even on solo projects, I use it — the separation of secrets alone is worth it.",[15,2309,2311],{"id":2310},"managing-secrets-correctly","Managing Secrets Correctly",[20,2313,2314],{},"Secrets in GitHub Actions live at three levels: repository secrets, environment secrets, and organization secrets. Use environment secrets for anything production-specific. Use repository secrets for things like NPM tokens that apply to all environments.",[20,2316,2317,2318,2321],{},"Set secrets in your repository under Settings > Secrets and variables > Actions. Reference them in workflows as ",[47,2319,2320],{},"${{ secrets.SECRET_NAME }}",". GitHub automatically masks secret values in logs.",[20,2323,2324,2325,2328],{},"What you should never do: hardcode values in workflow files, commit ",[47,2326,2327],{},".env"," files, or use the same secret across environments. Your production database password and your staging database password should be different secrets.",[15,2330,2332],{"id":2331},"caching-dependencies-efficiently","Caching Dependencies Efficiently",[20,2334,2335,2336,2339,2340,2343],{},"Beyond the built-in ",[47,2337,2338],{},"npm"," cache in ",[47,2341,2342],{},"setup-node",", you can cache other expensive operations. If you run database migrations or generate Prisma client during CI, cache those artifacts:",[57,2345,2347],{"className":1724,"code":2346,"language":1726,"meta":62,"style":62},"- name: Cache Prisma generated client\n uses: actions/cache@v4\n with:\n path: node_modules/.prisma\n key: ${{ runner.os }}-prisma-${{ hashFiles('prisma/schema.prisma') }}\n",[47,2348,2349,2361,2371,2377,2387],{"__ignoreMap":62},[66,2350,2351,2354,2356,2358],{"class":68,"line":69},[66,2352,2353],{"class":76},"- ",[66,2355,1733],{"class":213},[66,2357,87],{"class":76},[66,2359,2360],{"class":90},"Cache Prisma generated client\n",[66,2362,2363,2366,2368],{"class":68,"line":80},[66,2364,2365],{"class":213}," uses",[66,2367,87],{"class":76},[66,2369,2370],{"class":90},"actions/cache@v4\n",[66,2372,2373,2375],{"class":68,"line":97},[66,2374,2208],{"class":213},[66,2376,1750],{"class":76},[66,2378,2379,2382,2384],{"class":68,"line":128},[66,2380,2381],{"class":213}," path",[66,2383,87],{"class":76},[66,2385,2386],{"class":90},"node_modules/.prisma\n",[66,2388,2389,2392,2394],{"class":68,"line":141},[66,2390,2391],{"class":213}," key",[66,2393,87],{"class":76},[66,2395,2396],{"class":90},"${{ runner.os }}-prisma-${{ hashFiles('prisma/schema.prisma') }}\n",[20,2398,45,2399,2402,2403,2406],{},[47,2400,2401],{},"hashFiles"," function generates a cache key based on file content. When ",[47,2404,2405],{},"schema.prisma"," changes, the cache invalidates automatically. When it has not changed, you skip the generation step entirely.",[15,2408,2410],{"id":2409},"matrix-builds-for-multi-version-testing","Matrix Builds for Multi-Version Testing",[20,2412,2413],{},"If you need to verify compatibility across Node versions or operating systems:",[57,2415,2417],{"className":1724,"code":2416,"language":1726,"meta":62,"style":62},"strategy:\n matrix:\n node-version: [18, 20, 22]\n os: [ubuntu-latest, windows-latest]\n\nRuns-on: ${{ matrix.os }}\nsteps:\n - uses: actions/setup-node@v4\n with:\n node-version: ${{ matrix.node-version }}\n",[47,2418,2419,2426,2433,2453,2470,2474,2484,2491,2501,2507],{"__ignoreMap":62},[66,2420,2421,2424],{"class":68,"line":69},[66,2422,2423],{"class":213},"strategy",[66,2425,1750],{"class":76},[66,2427,2428,2431],{"class":68,"line":80},[66,2429,2430],{"class":213}," matrix",[66,2432,1750],{"class":76},[66,2434,2435,2437,2439,2442,2444,2447,2449,2451],{"class":68,"line":97},[66,2436,2215],{"class":213},[66,2438,1765],{"class":76},[66,2440,2441],{"class":83},"18",[66,2443,479],{"class":76},[66,2445,2446],{"class":83},"20",[66,2448,479],{"class":76},[66,2450,415],{"class":83},[66,2452,1770],{"class":76},[66,2454,2455,2458,2460,2463,2465,2468],{"class":68,"line":128},[66,2456,2457],{"class":213}," os",[66,2459,1765],{"class":76},[66,2461,2462],{"class":90},"ubuntu-latest",[66,2464,479],{"class":76},[66,2466,2467],{"class":90},"windows-latest",[66,2469,1770],{"class":76},[66,2471,2472],{"class":68,"line":141},[66,2473,459],{"emptyLinePlaceholder":458},[66,2475,2476,2479,2481],{"class":68,"line":259},[66,2477,2478],{"class":213},"Runs-on",[66,2480,87],{"class":76},[66,2482,2483],{"class":90},"${{ matrix.os }}\n",[66,2485,2486,2489],{"class":68,"line":265},[66,2487,2488],{"class":213},"steps",[66,2490,1750],{"class":76},[66,2492,2493,2495,2497,2499],{"class":68,"line":450},[66,2494,2180],{"class":76},[66,2496,2183],{"class":213},[66,2498,87],{"class":76},[66,2500,2203],{"class":90},[66,2502,2503,2505],{"class":68,"line":455},[66,2504,2208],{"class":213},[66,2506,1750],{"class":76},[66,2508,2509,2511,2513],{"class":68,"line":462},[66,2510,2215],{"class":213},[66,2512,87],{"class":76},[66,2514,2515],{"class":90},"${{ matrix.node-version }}\n",[20,2517,2518],{},"Matrix builds run in parallel, so six combinations take roughly the same time as one. Use this when you ship a library or CLI that needs to support multiple environments. For application code targeting a single deployment environment, matrices add noise.",[15,2520,2522],{"id":2521},"reusable-workflows","Reusable Workflows",[20,2524,2525,2526,2529],{},"When you manage multiple repositories, you will find yourself duplicating CI config. GitHub supports reusable workflows via the ",[47,2527,2528],{},"workflow_call"," trigger.",[20,2531,2532,2533,2536],{},"Define a reusable workflow in a dedicated ",[47,2534,2535],{},".github/workflows/"," file:",[57,2538,2540],{"className":1724,"code":2539,"language":1726,"meta":62,"style":62},"# .github/workflows/node-ci.yml\non:\n workflow_call:\n inputs:\n node-version:\n required: false\n type: string\n default: \"20\"\n",[47,2541,2542,2547,2554,2561,2568,2574,2584,2593],{"__ignoreMap":62},[66,2543,2544],{"class":68,"line":69},[66,2545,2546],{"class":582},"# .github/workflows/node-ci.yml\n",[66,2548,2549,2552],{"class":68,"line":80},[66,2550,2551],{"class":83},"on",[66,2553,1750],{"class":76},[66,2555,2556,2559],{"class":68,"line":97},[66,2557,2558],{"class":213}," workflow_call",[66,2560,1750],{"class":76},[66,2562,2563,2566],{"class":68,"line":128},[66,2564,2565],{"class":213}," inputs",[66,2567,1750],{"class":76},[66,2569,2570,2572],{"class":68,"line":141},[66,2571,2215],{"class":213},[66,2573,1750],{"class":76},[66,2575,2576,2579,2581],{"class":68,"line":259},[66,2577,2578],{"class":213}," required",[66,2580,87],{"class":76},[66,2582,2583],{"class":83},"false\n",[66,2585,2586,2588,2590],{"class":68,"line":265},[66,2587,251],{"class":213},[66,2589,87],{"class":76},[66,2591,2592],{"class":90},"string\n",[66,2594,2595,2598,2600],{"class":68,"line":450},[66,2596,2597],{"class":213}," default",[66,2599,87],{"class":76},[66,2601,2602],{"class":90},"\"20\"\n",[20,2604,2605],{},"Then call it from any repository:",[57,2607,2609],{"className":1724,"code":2608,"language":1726,"meta":62,"style":62},"jobs:\n ci:\n uses: your-org/.github/.github/workflows/node-ci.yml@main\n with:\n node-version: \"20\"\n secrets: inherit\n",[47,2610,2611,2618,2625,2634,2640,2648],{"__ignoreMap":62},[66,2612,2613,2616],{"class":68,"line":69},[66,2614,2615],{"class":213},"jobs",[66,2617,1750],{"class":76},[66,2619,2620,2623],{"class":68,"line":80},[66,2621,2622],{"class":213}," ci",[66,2624,1750],{"class":76},[66,2626,2627,2629,2631],{"class":68,"line":97},[66,2628,2365],{"class":213},[66,2630,87],{"class":76},[66,2632,2633],{"class":90},"your-org/.github/.github/workflows/node-ci.yml@main\n",[66,2635,2636,2638],{"class":68,"line":128},[66,2637,2208],{"class":213},[66,2639,1750],{"class":76},[66,2641,2642,2644,2646],{"class":68,"line":141},[66,2643,2215],{"class":213},[66,2645,87],{"class":76},[66,2647,2602],{"class":90},[66,2649,2650,2653,2655],{"class":68,"line":259},[66,2651,2652],{"class":213}," secrets",[66,2654,87],{"class":76},[66,2656,2657],{"class":90},"inherit\n",[20,2659,2660],{},"This lets you maintain one canonical CI definition and pull it into every project. When you improve the workflow, every project benefits immediately.",[15,2662,2664],{"id":2663},"handling-deployment-rollbacks","Handling Deployment Rollbacks",[20,2666,2667],{},"Automated deployment is only half the problem. The other half is knowing what to do when deployment breaks production. Your workflow should capture the deployment artifact version:",[57,2669,2671],{"className":1724,"code":2670,"language":1726,"meta":62,"style":62},"- name: Tag deployment\n run: |\n git tag \"deploy-$(date +%Y%m%d%H%M%S)\" ${{ github.sha }}\n git push origin --tags\n",[47,2672,2673,2684,2692,2697],{"__ignoreMap":62},[66,2674,2675,2677,2679,2681],{"class":68,"line":69},[66,2676,2353],{"class":76},[66,2678,1733],{"class":213},[66,2680,87],{"class":76},[66,2682,2683],{"class":90},"Tag deployment\n",[66,2685,2686,2688,2690],{"class":68,"line":80},[66,2687,2277],{"class":213},[66,2689,87],{"class":76},[66,2691,2282],{"class":72},[66,2693,2694],{"class":68,"line":97},[66,2695,2696],{"class":90}," git tag \"deploy-$(date +%Y%m%d%H%M%S)\" ${{ github.sha }}\n",[66,2698,2699],{"class":68,"line":128},[66,2700,2701],{"class":90}," git push origin --tags\n",[20,2703,2704],{},"When something goes wrong, you know exactly which commit is live and can revert to the previous tag. Some platforms — Fly.io, Railway, Vercel — maintain deployment history natively. In that case, rollback is a CLI command. For self-hosted deployments, the git tag approach gives you the same audit trail.",[15,2706,2708],{"id":2707},"the-workflow-hygiene-rules-i-enforce","The Workflow Hygiene Rules I Enforce",[20,2710,2711,2712,2715,2716,2719,2720,1138],{},"Pin action versions to a commit SHA, not a mutable tag. ",[47,2713,2714],{},"actions/checkout@v4"," can change without you knowing. ",[47,2717,2718],{},"actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683"," cannot. GitHub Dependabot can keep these updated automatically — enable it in ",[47,2721,2722],{},".github/dependabot.yml",[20,2724,2725],{},"Keep workflows focused. A workflow file that handles CI, deployment, release notes, and dependency updates is a maintenance burden. One workflow, one purpose.",[20,2727,2728],{},"Add a concurrency group to your deploy workflow to cancel in-progress deployments when a new push arrives:",[57,2730,2732],{"className":1724,"code":2731,"language":1726,"meta":62,"style":62},"concurrency:\n group: production\n cancel-in-progress: true\n",[47,2733,2734,2741,2750],{"__ignoreMap":62},[66,2735,2736,2739],{"class":68,"line":69},[66,2737,2738],{"class":213},"concurrency",[66,2740,1750],{"class":76},[66,2742,2743,2746,2748],{"class":68,"line":80},[66,2744,2745],{"class":213}," group",[66,2747,87],{"class":76},[66,2749,2164],{"class":90},[66,2751,2752,2755,2757],{"class":68,"line":97},[66,2753,2754],{"class":213}," cancel-in-progress",[66,2756,87],{"class":76},[66,2758,2759],{"class":83},"true\n",[20,2761,2762],{},"This prevents race conditions where an older, slower deployment overwrites a newer one.",[15,2764,2766],{"id":2765},"starting-point-for-new-projects","Starting Point for New Projects",[20,2768,2769,2770,2772,2773,2776],{},"Every new project I start gets a ",[47,2771,2535],{}," directory with a ",[47,2774,2775],{},"ci.yml"," on day one. Not after the first bug. Not when the team grows. Day one. The cost of adding CI after the fact — retrofitting tests, untangling implicit environment dependencies — is always higher than building it in from the start.",[20,2778,2779],{},"GitHub Actions has made solid CI/CD accessible to every team regardless of size. There is no excuse for manual deployments in 2026.",[40,2781],{},[20,2783,2784,2785,1138],{},"Want help designing a CI/CD pipeline that fits your team's workflow? Let's talk. Book a session at ",[690,2786,692],{"href":692,"rel":2787},[694],[40,2789],{},[15,2791,702],{"id":701},[304,2793,2794,2800,2806,2812],{},[307,2795,2796],{},[690,2797,2799],{"href":2798},"/blog/continuous-deployment-guide","Continuous Deployment: From Code Push to Production in Minutes",[307,2801,2802],{},[690,2803,2805],{"href":2804},"/blog/zero-to-production-nuxt-vercel","Zero to Production: My Nuxt + Vercel Deployment Pipeline",[307,2807,2808],{},[690,2809,2811],{"href":2810},"/blog/github-best-practices","GitHub Best Practices: Branch Strategy, PRs, and Repo Organization",[307,2813,2814],{},[690,2815,2817],{"href":2816},"/blog/logging-production-apps","Structured Logging for Production: The Setup You'll Thank Yourself For",[730,2819,2820],{},"html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":62,"searchDepth":97,"depth":97,"links":2822},[2823,2827,2828,2829,2830,2831,2832,2833,2834],{"id":1705,"depth":80,"text":1706,"children":2824},[2825,2826],{"id":1713,"depth":97,"text":1714},{"id":2071,"depth":97,"text":2072},{"id":2310,"depth":80,"text":2311},{"id":2331,"depth":80,"text":2332},{"id":2409,"depth":80,"text":2410},{"id":2521,"depth":80,"text":2522},{"id":2663,"depth":80,"text":2664},{"id":2707,"depth":80,"text":2708},{"id":2765,"depth":80,"text":2766},{"id":701,"depth":80,"text":702},"DevOps","Set up GitHub Actions CI/CD pipelines from scratch — automated testing, builds, and deployments that actually work in production environments.",[2838,2839],"GitHub Actions CI/CD","continuous integration",{},"/blog/github-actions-cicd-guide",{"title":1689,"description":2836},"blog/github-actions-cicd-guide",[2845,2846,2835,2847],"GitHub Actions","CI/CD","Automation","n6GJbB3_ueUZs0zQ0fUS0eDgxW1hBvkDlDkBy4z7lxk",{"id":2850,"title":2811,"author":2851,"body":2852,"category":2835,"date":746,"description":3343,"extension":748,"featured":749,"image":750,"keywords":3344,"meta":3347,"navigation":458,"path":2810,"readTime":265,"seo":3348,"stem":3349,"tags":3350,"__hash__":3354},"blog/blog/github-best-practices.md",{"name":9,"bio":10},{"type":12,"value":2853,"toc":3333},[2854,2857,2860,2863,2867,2870,2884,2887,2890,2893,2897,2900,2908,2911,2914,2963,2967,2970,2976,2982,2985,3068,3075,3084,3090,3094,3097,3104,3107,3110,3114,3117,3123,3126,3132,3135,3139,3142,3246,3253,3256,3260,3265,3291,3294,3296,3302,3304,3306,3330],[1694,2855,2811],{"id":2856},"github-best-practices-branch-strategy-prs-and-repo-organization",[20,2858,2859],{},"A Git workflow is one of those things that seems unimportant until the team grows to three people and suddenly merge conflicts are daily, nobody can track which features are in which release, and the main branch breaks every other day because someone pushed untested code directly.",[20,2861,2862],{},"The practices I describe here are not theoretical. They are the patterns I have seen work across teams ranging from three engineers to fifty. They are also not dogma — adapt them to your context.",[15,2864,2866],{"id":2865},"branch-strategy-trunk-based-development","Branch Strategy: Trunk-Based Development",[20,2868,2869],{},"There are three common branch strategies: Git Flow, GitHub Flow, and trunk-based development. My recommendation is trunk-based development for most teams, and here is why.",[20,2871,2872,2873,479,2876,2879,2880,2883],{},"Git Flow has long-lived ",[47,2874,2875],{},"develop",[47,2877,2878],{},"release",", and ",[47,2881,2882],{},"hotfix"," branches. It made sense when deployments were scheduled events. For teams shipping to production multiple times a day, the overhead of maintaining multiple long-lived branches and merging between them adds friction without corresponding benefit.",[20,2885,2886],{},"GitHub Flow is simple: one main branch, short-lived feature branches, merge to main. This works well but can lead to integration problems when feature branches live for more than a few days.",[20,2888,2889],{},"Trunk-based development takes GitHub Flow further: main is always deployable, feature branches are extremely short-lived (1-3 days maximum), and features that are not ready for users are hidden behind feature flags rather than on a separate branch. Integration happens continuously, not at a \"release merge\" moment.",[20,2891,2892],{},"The discipline trunk-based development requires: feature flags for incomplete features, a CI pipeline that must pass before merging, and a culture where nobody ships untested code because main is always live.",[15,2894,2896],{"id":2895},"branch-naming-conventions","Branch Naming Conventions",[20,2898,2899],{},"Whatever strategy you use, consistent branch naming makes your repository navigable:",[57,2901,2906],{"className":2902,"code":2904,"language":2905},[2903],"language-text","feature/TICKET-123-user-authentication\nfix/TICKET-456-login-redirect-loop\nchore/update-dependencies\ndocs/api-documentation\nrefactor/payment-service-cleanup\n","text",[47,2907,2904],{"__ignoreMap":62},[20,2909,2910],{},"The prefix indicates the type of change. The ticket number (if you use one) provides traceability to your issue tracker. The description makes the branch's purpose clear from the name alone.",[20,2912,2913],{},"Enforce this with a Git hook or a GitHub Actions workflow that validates branch names:",[57,2915,2917],{"className":1724,"code":2916,"language":1726,"meta":62,"style":62},"- name: Validate branch name\n run: |\n BRANCH_NAME=\"${{ github.head_ref }}\"\n if ! echo \"$BRANCH_NAME\" | grep -qE \"^(feature|fix|chore|docs|refactor)/\"; then\n echo \"Branch name '$BRANCH_NAME' does not follow naming convention\"\n exit 1\n fi\n",[47,2918,2919,2930,2938,2943,2948,2953,2958],{"__ignoreMap":62},[66,2920,2921,2923,2925,2927],{"class":68,"line":69},[66,2922,2353],{"class":76},[66,2924,1733],{"class":213},[66,2926,87],{"class":76},[66,2928,2929],{"class":90},"Validate branch name\n",[66,2931,2932,2934,2936],{"class":68,"line":80},[66,2933,2277],{"class":213},[66,2935,87],{"class":76},[66,2937,2282],{"class":72},[66,2939,2940],{"class":68,"line":97},[66,2941,2942],{"class":90}," BRANCH_NAME=\"${{ github.head_ref }}\"\n",[66,2944,2945],{"class":68,"line":128},[66,2946,2947],{"class":90}," if ! echo \"$BRANCH_NAME\" | grep -qE \"^(feature|fix|chore|docs|refactor)/\"; then\n",[66,2949,2950],{"class":68,"line":141},[66,2951,2952],{"class":90}," echo \"Branch name '$BRANCH_NAME' does not follow naming convention\"\n",[66,2954,2955],{"class":68,"line":259},[66,2956,2957],{"class":90}," exit 1\n",[66,2959,2960],{"class":68,"line":265},[66,2961,2962],{"class":90}," fi\n",[15,2964,2966],{"id":2965},"pull-request-discipline","Pull Request Discipline",[20,2968,2969],{},"The pull request is where code review happens. The quality of your PR process directly affects the quality of code in production. A few principles I enforce:",[20,2971,2972,2975],{},[26,2973,2974],{},"Small, focused PRs."," A PR that touches 15 files and changes 800 lines gets cursory reviews because reviewing it thoroughly would take three hours. A PR that changes 3 files and 100 lines gets real review because it is possible to understand completely. Aim for PRs that take under 30 minutes to review thoroughly. If your change requires more scope, break it into sequential PRs.",[20,2977,2978,2981],{},[26,2979,2980],{},"PRs have a clear description."," Your PR description should explain what the change does, why it is being made, and how to test it. Reviewers should not need to read every line of code to understand the purpose of a PR.",[20,2983,2984],{},"A PR template enforces this:",[57,2986,2990],{"className":2987,"code":2988,"language":2989,"meta":62,"style":62},"language-markdown shiki shiki-themes github-dark","## What\nBrief description of the change.\n\n## Why\nContext for why this change is being made. Link to issue/ticket.\n\n## How to Test\n1. Steps to verify the change works correctly\n2. Edge cases to check\n\n## Screenshots (if UI change)\n\n## Checklist\n- [ ] Tests added/updated\n- [ ] Documentation updated if needed\n- [ ] Migrations applied if needed\n","markdown",[47,2991,2992,2997,3002,3006,3011,3016,3020,3025,3030,3035,3039,3044,3048,3053,3058,3063],{"__ignoreMap":62},[66,2993,2994],{"class":68,"line":69},[66,2995,2996],{},"## What\n",[66,2998,2999],{"class":68,"line":80},[66,3000,3001],{},"Brief description of the change.\n",[66,3003,3004],{"class":68,"line":97},[66,3005,459],{"emptyLinePlaceholder":458},[66,3007,3008],{"class":68,"line":128},[66,3009,3010],{},"## Why\n",[66,3012,3013],{"class":68,"line":141},[66,3014,3015],{},"Context for why this change is being made. Link to issue/ticket.\n",[66,3017,3018],{"class":68,"line":259},[66,3019,459],{"emptyLinePlaceholder":458},[66,3021,3022],{"class":68,"line":265},[66,3023,3024],{},"## How to Test\n",[66,3026,3027],{"class":68,"line":450},[66,3028,3029],{},"1. Steps to verify the change works correctly\n",[66,3031,3032],{"class":68,"line":455},[66,3033,3034],{},"2. Edge cases to check\n",[66,3036,3037],{"class":68,"line":462},[66,3038,459],{"emptyLinePlaceholder":458},[66,3040,3041],{"class":68,"line":470},[66,3042,3043],{},"## Screenshots (if UI change)\n",[66,3045,3046],{"class":68,"line":491},[66,3047,459],{"emptyLinePlaceholder":458},[66,3049,3050],{"class":68,"line":1175},[66,3051,3052],{},"## Checklist\n",[66,3054,3055],{"class":68,"line":1183},[66,3056,3057],{},"- [ ] Tests added/updated\n",[66,3059,3060],{"class":68,"line":1191},[66,3061,3062],{},"- [ ] Documentation updated if needed\n",[66,3064,3065],{"class":68,"line":1846},[66,3066,3067],{},"- [ ] Migrations applied if needed\n",[20,3069,3070,3071,3074],{},"Check this template in at ",[47,3072,3073],{},".github/PULL_REQUEST_TEMPLATE.md",". GitHub automatically populates the PR description with it.",[20,3076,3077,3080,3081,3083],{},[26,3078,3079],{},"Require CI to pass before merge."," Configure branch protection rules on ",[47,3082,1720],{}," to require your CI workflow to pass. Under Settings > Branches > Branch protection rules, enable \"Require status checks to pass before merging.\" Select your CI workflow as a required check. This is the mechanical enforcement of \"main is always green.\"",[20,3085,3086,3089],{},[26,3087,3088],{},"Require at least one code review."," Also in branch protection rules, enable \"Require a pull request before merging\" and \"Require approvals: 1.\" Solo developers can use a self-review workflow or require no approvals, but once you have more than one engineer, human code review is a quality gate worth the friction.",[15,3091,3093],{"id":3092},"code-review-culture","Code Review Culture",[20,3095,3096],{},"The technical side of code review is straightforward: check for correctness, performance issues, security vulnerabilities, test coverage, and adherence to your patterns. The cultural side is what determines whether code review is useful.",[20,3098,3099,3100,3103],{},"Code review feedback should be about code, not about the author. \"This query will do a sequential scan on large tables — an index on ",[47,3101,3102],{},"user_id"," would improve this significantly\" is helpful feedback. \"Why would you write it this way?\" is not.",[20,3105,3106],{},"Distinguish between required changes and suggestions. Mark required changes clearly. Flag stylistic preferences or alternatives as non-blocking. Reviewers who block merges on personal style preferences slow the team down without improving quality.",[20,3108,3109],{},"Respond to review comments promptly. A PR that sits waiting for the author's response to a comment for three days creates merge conflicts and loses its reviewer's context. Treat open PRs as work in progress until they merge.",[15,3111,3113],{"id":3112},"repository-organization","Repository Organization",[20,3115,3116],{},"For a single-team project, a straightforward structure is usually best:",[57,3118,3121],{"className":3119,"code":3120,"language":2905},[2903],"myapp/\n├── .github/\n│ ├── workflows/ # GitHub Actions\n│ ├── PULL_REQUEST_TEMPLATE.md\n│ └── CODEOWNERS\n├── src/ # Application code\n├── tests/ # Test files\n├── docs/ # Documentation\n├── docker-compose.yml\n├── .env.example\n└── README.md\n",[47,3122,3120],{"__ignoreMap":62},[20,3124,3125],{},"CODEOWNERS is underused. It maps file paths to the GitHub users or teams who must review changes to those files. Changes to your authentication module should require review from someone who owns that code:",[57,3127,3130],{"className":3128,"code":3129,"language":2905},[2903],"# .github/CODEOWNERS\nsrc/auth/ @myorg/auth-team\nsrc/payments/ @myorg/payment-team\ninfrastructure/ @myorg/devops-team\n",[47,3131,3129],{"__ignoreMap":62},[20,3133,3134],{},"When a PR touches a file covered by CODEOWNERS, GitHub automatically requests a review from the owner. This ensures the right people review changes to sensitive or complex code.",[15,3136,3138],{"id":3137},"managing-issues-and-projects","Managing Issues and Projects",[20,3140,3141],{},"Use GitHub Issues for bug tracking and feature requests. Create issue templates for different types:",[57,3143,3145],{"className":2987,"code":3144,"language":2989,"meta":62,"style":62},"\u003C!-- .github/ISSUE_TEMPLATE/bug_report.md -->\n---\nname: Bug Report\nabout: Report a reproducible bug\nlabels: bug\n---\n\n## Description\nWhat went wrong?\n\n## Steps to Reproduce\n1.\n2.\n\n## Expected Behavior\n\n## Actual Behavior\n\n## Environment\n- Browser/OS:\n- Application version:\n",[47,3146,3147,3152,3157,3162,3167,3172,3176,3180,3185,3190,3194,3199,3204,3209,3213,3218,3222,3227,3231,3236,3241],{"__ignoreMap":62},[66,3148,3149],{"class":68,"line":69},[66,3150,3151],{},"\u003C!-- .github/ISSUE_TEMPLATE/bug_report.md -->\n",[66,3153,3154],{"class":68,"line":80},[66,3155,3156],{},"---\n",[66,3158,3159],{"class":68,"line":97},[66,3160,3161],{},"name: Bug Report\n",[66,3163,3164],{"class":68,"line":128},[66,3165,3166],{},"about: Report a reproducible bug\n",[66,3168,3169],{"class":68,"line":141},[66,3170,3171],{},"labels: bug\n",[66,3173,3174],{"class":68,"line":259},[66,3175,3156],{},[66,3177,3178],{"class":68,"line":265},[66,3179,459],{"emptyLinePlaceholder":458},[66,3181,3182],{"class":68,"line":450},[66,3183,3184],{},"## Description\n",[66,3186,3187],{"class":68,"line":455},[66,3188,3189],{},"What went wrong?\n",[66,3191,3192],{"class":68,"line":462},[66,3193,459],{"emptyLinePlaceholder":458},[66,3195,3196],{"class":68,"line":470},[66,3197,3198],{},"## Steps to Reproduce\n",[66,3200,3201],{"class":68,"line":491},[66,3202,3203],{},"1.\n",[66,3205,3206],{"class":68,"line":1175},[66,3207,3208],{},"2.\n",[66,3210,3211],{"class":68,"line":1183},[66,3212,459],{"emptyLinePlaceholder":458},[66,3214,3215],{"class":68,"line":1191},[66,3216,3217],{},"## Expected Behavior\n",[66,3219,3220],{"class":68,"line":1846},[66,3221,459],{"emptyLinePlaceholder":458},[66,3223,3224],{"class":68,"line":1854},[66,3225,3226],{},"## Actual Behavior\n",[66,3228,3229],{"class":68,"line":1865},[66,3230,459],{"emptyLinePlaceholder":458},[66,3232,3233],{"class":68,"line":1876},[66,3234,3235],{},"## Environment\n",[66,3237,3238],{"class":68,"line":1887},[66,3239,3240],{},"- Browser/OS:\n",[66,3242,3243],{"class":68,"line":1893},[66,3244,3245],{},"- Application version:\n",[20,3247,3248,3249,3252],{},"Link issues to PRs with ",[47,3250,3251],{},"Fixes #123"," in the PR description. GitHub automatically closes the issue when the PR merges and creates a traceability link in both directions.",[20,3254,3255],{},"GitHub Projects is viable for kanban-style sprint planning if you are already living in GitHub. For more sophisticated project management, Jira or Linear may serve you better, but the overhead of maintaining both GitHub Issues and an external tracker is real — choose one as the source of truth.",[15,3257,3259],{"id":3258},"protected-main-branch-configuration","Protected Main Branch Configuration",[20,3261,3262,3263,152],{},"The complete branch protection configuration for ",[47,3264,1720],{},[304,3266,3267,3270,3273,3276,3279,3282,3285,3288],{},[307,3268,3269],{},"Require a pull request before merging",[307,3271,3272],{},"Require approvals: 1 (or 2 for larger teams)",[307,3274,3275],{},"Dismiss stale pull request approvals when new commits are pushed",[307,3277,3278],{},"Require review from Code Owners",[307,3280,3281],{},"Require status checks to pass before merging (select your CI checks)",[307,3283,3284],{},"Require branches to be up to date before merging",[307,3286,3287],{},"Do not allow force pushes",[307,3289,3290],{},"Do not allow deletions",[20,3292,3293],{},"This configuration enforces that every change to main goes through review, passes CI, and is current with main at the time of merge. It is the mechanical backbone of a professional Git workflow.",[40,3295],{},[20,3297,3298,3299,1138],{},"If you want help establishing Git and GitHub workflows for your team, or want a second opinion on your current process, book a session at ",[690,3300,692],{"href":692,"rel":3301},[694],[40,3303],{},[15,3305,702],{"id":701},[304,3307,3308,3314,3318,3324],{},[307,3309,3310],{},[690,3311,3313],{"href":3312},"/blog/vercel-deployment-best-practices","Vercel Deployment Best Practices: Shipping With Confidence",[307,3315,3316],{},[690,3317,1689],{"href":2841},[307,3319,3320],{},[690,3321,3323],{"href":3322},"/blog/ssl-tls-best-practices","SSL/TLS Configuration Best Practices in 2026",[307,3325,3326],{},[690,3327,3329],{"href":3328},"/blog/performance-monitoring-guide","Application Performance Monitoring: Beyond the Health Check Endpoint",[730,3331,3332],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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":62,"searchDepth":97,"depth":97,"links":3334},[3335,3336,3337,3338,3339,3340,3341,3342],{"id":2865,"depth":80,"text":2866},{"id":2895,"depth":80,"text":2896},{"id":2965,"depth":80,"text":2966},{"id":3092,"depth":80,"text":3093},{"id":3112,"depth":80,"text":3113},{"id":3137,"depth":80,"text":3138},{"id":3258,"depth":80,"text":3259},{"id":701,"depth":80,"text":702},"GitHub best practices for engineering teams — branch naming conventions, pull request workflows, code review culture, and repository organization that scales.",[3345,3346],"GitHub best practices","Git workflow",{},{"title":2811,"description":3343},"blog/github-best-practices",[3351,3352,2835,3353],"GitHub","Git","Team","lL7jYnApW_MwYXH0d2TrWlnT34bAGi6CalRaqPHZ8Dw",{"id":3356,"title":3357,"author":3358,"body":3359,"category":4112,"date":746,"description":4113,"extension":748,"featured":749,"image":750,"keywords":4114,"meta":4120,"navigation":458,"path":4121,"readTime":462,"seo":4122,"stem":4123,"tags":4124,"__hash__":4129},"blog/blog/hexagonal-architecture-guide.md","Hexagonal Architecture: Ports, Adapters, and the Core That Never Changes",{"name":9,"bio":10},{"type":12,"value":3360,"toc":4102},[3361,3365,3368,3371,3374,3376,3380,3387,3390,3396,3414,3417,3419,3423,3430,3436,3463,3474,3477,3497,3504,3506,3510,3513,3519,3522,3524,3528,3531,3534,3548,3551,3567,3975,3978,3981,3996,3999,4001,4005,4008,4014,4020,4026,4028,4032,4035,4049,4052,4055,4057,4060,4062,4069,4071,4073,4099],[15,3362,3364],{"id":3363},"the-origin-and-the-name","The Origin and the Name",[20,3366,3367],{},"Hexagonal architecture was introduced by Alistair Cockburn in 2005. The hexagon in the name is not meaningful — Cockburn chose a hexagonal shape because it's visually distinct and has enough sides to show multiple ports around the perimeter. It could just as easily be called \"octagonal architecture\" and mean the same thing.",[20,3369,3370],{},"What is meaningful is the core insight: your application's domain logic should be at the center, entirely independent of the infrastructure that surrounds it. Databases, web frameworks, message queues, external APIs — these are all interchangeable details at the edge of the system. The domain core should be able to function without knowing what those details are.",[20,3372,3373],{},"This insight is shared with clean architecture and onion architecture. Hexagonal architecture makes it concrete through two specific concepts: ports and adapters.",[40,3375],{},[15,3377,3379],{"id":3378},"ports-what-your-application-needs-and-provides","Ports: What Your Application Needs and Provides",[20,3381,3382,3383,3386],{},"A ",[26,3384,3385],{},"port"," is an interface that defines a communication boundary.",[20,3388,3389],{},"There are two kinds of ports:",[20,3391,3392,3395],{},[26,3393,3394],{},"Primary ports (driving ports):"," These are how the outside world interacts with your application. An HTTP controller driving a use case. A CLI command triggering a domain operation. A scheduled job triggering a business process. The primary port is the entry point — it drives the application.",[20,3397,3398,3401,3402,3405,3406,3409,3410,3413],{},[26,3399,3400],{},"Secondary ports (driven ports):"," These are interfaces your application uses to interact with the outside world. ",[47,3403,3404],{},"OrderRepository"," (your application drives the database). ",[47,3407,3408],{},"EmailService"," (your application drives the email provider). ",[47,3411,3412],{},"PaymentGateway"," (your application drives the payment processor). Secondary ports are defined by the application domain — they specify exactly what the domain needs, in domain terms.",[20,3415,3416],{},"The critical distinction: primary ports face inward (the world drives your app). Secondary ports face outward (your app drives the world), but they are defined by the domain, not by the infrastructure.",[40,3418],{},[15,3420,3422],{"id":3421},"adapters-the-pluggable-implementations","Adapters: The Pluggable Implementations",[20,3424,3425,3426,3429],{},"An ",[26,3427,3428],{},"adapter"," is a concrete implementation of a port that translates between the domain's language and the infrastructure's language.",[20,3431,3432,3433,3435],{},"For a secondary port like ",[47,3434,3404],{},", you might have:",[304,3437,3438,3447,3455],{},[307,3439,3440,3443,3444,3446],{},[47,3441,3442],{},"PrismaOrderRepository"," — implements ",[47,3445,3404],{}," using Prisma and PostgreSQL",[307,3448,3449,3443,3452,3454],{},[47,3450,3451],{},"InMemoryOrderRepository",[47,3453,3404],{}," using an in-memory Map (for tests)",[307,3456,3457,3443,3460,3462],{},[47,3458,3459],{},"DynamoOrderRepository",[47,3461,3404],{}," using AWS DynamoDB",[20,3464,3465,3466,3469,3470,3473],{},"Each adapter translates between what the domain needs (save an ",[47,3467,3468],{},"Order"," domain object) and what the infrastructure provides (upsert a row in a PostgreSQL table). The domain calls ",[47,3471,3472],{},"orderRepo.save(order)"," and doesn't know — or care — which adapter is behind the interface.",[20,3475,3476],{},"For a primary port driven by HTTP:",[304,3478,3479,3485,3491],{},[307,3480,3481,3484],{},[47,3482,3483],{},"ExpressOrderController"," — adapts HTTP requests to use case method calls",[307,3486,3487,3490],{},[47,3488,3489],{},"GraphQLOrderResolver"," — adapts GraphQL queries/mutations to the same use case",[307,3492,3493,3496],{},[47,3494,3495],{},"CLIOrderCommand"," — adapts command-line input to the same use case",[20,3498,3499,3500,3503],{},"The same domain use case (",[47,3501,3502],{},"CreateOrderUseCase",") can be driven by any primary adapter. The use case doesn't know it's handling an HTTP request vs a CLI command.",[40,3505],{},[15,3507,3509],{"id":3508},"the-practical-structure","The Practical Structure",[20,3511,3512],{},"Let me show what this looks like in a real project directory structure:",[57,3514,3517],{"className":3515,"code":3516,"language":2905},[2903],"src/\n├── domain/\n│ ├── order.ts ← Order entity and business rules\n│ ├── orderItem.ts\n│ ├── money.ts\n│ └── ports/\n│ ├── orderRepository.ts ← interface (secondary port)\n│ └── paymentGateway.ts ← interface (secondary port)\n│\n├── application/\n│ ├── createOrder.ts ← use case (primary port implementation target)\n│ ├── submitOrder.ts\n│ └── cancelOrder.ts\n│\n├── adapters/\n│ ├── driving/ ← primary adapters\n│ │ ├── http/\n│ │ │ └── orderController.ts\n│ │ └── cli/\n│ │ └── orderCommand.ts\n│ └── driven/ ← secondary adapters\n│ ├── persistence/\n│ │ ├── prismaOrderRepository.ts\n│ │ └── inMemoryOrderRepository.ts\n│ └── payments/\n│ ├── stripePaymentGateway.ts\n│ └── mockPaymentGateway.ts\n│\n└── main.ts ← composition root (wires everything together)\n",[47,3518,3516],{"__ignoreMap":62},[20,3520,3521],{},"The domain and application layers have no imports from the adapters layer. They only import from each other. The adapters layer imports from the domain (to know what interfaces to implement) but the domain never imports from adapters. This is the dependency rule enforced in code.",[40,3523],{},[15,3525,3527],{"id":3526},"the-testability-benefit-why-this-changes-everything","The Testability Benefit: Why This Changes Everything",[20,3529,3530],{},"The practical reason to adopt hexagonal architecture is that it makes testing dramatically better.",[20,3532,3533],{},"Without hexagonal architecture, a test of business logic typically requires:",[304,3535,3536,3539,3542,3545],{},[307,3537,3538],{},"A running database (because the domain uses the ORM directly)",[307,3540,3541],{},"A running web server (because the logic is in the route handler)",[307,3543,3544],{},"Real HTTP requests (because the framework is woven into the logic)",[307,3546,3547],{},"Elaborate setup and teardown",[20,3549,3550],{},"With hexagonal architecture, a test of business logic requires:",[304,3552,3553,3558,3561,3564],{},[307,3554,3425,3555,3557],{},[47,3556,3451],{}," that you control completely",[307,3559,3560],{},"Direct instantiation of the use case with the test repository",[307,3562,3563],{},"Method calls instead of HTTP requests",[307,3565,3566],{},"No external dependencies",[57,3568,3572],{"className":3569,"code":3570,"language":3571,"meta":62,"style":62},"language-typescript shiki shiki-themes github-dark","// Test for CreateOrderUseCase — no database, no HTTP, no framework\ndescribe('CreateOrderUseCase', () => {\n let orderRepo: InMemoryOrderRepository\n let productRepo: InMemoryProductRepository\n let useCase: CreateOrderUseCase\n\n beforeEach(() => {\n orderRepo = new InMemoryOrderRepository()\n productRepo = new InMemoryProductRepository()\n productRepo.add(new Product('prod-1', 'Widget', Money.of(29.99)))\n useCase = new CreateOrderUseCase(orderRepo, productRepo)\n })\n\n it('creates an order with valid items', async () => {\n const orderId = await useCase.execute({\n customerId: 'cust-1',\n items: [{ productId: 'prod-1', quantity: 2 }]\n })\n\n const order = await orderRepo.findById(orderId)\n expect(order).toBeDefined()\n expect(order!.getTotal()).toEqual(Money.of(59.98))\n })\n\n it('throws when product does not exist', async () => {\n await expect(useCase.execute({\n customerId: 'cust-1',\n items: [{ productId: 'nonexistent', quantity: 1 }]\n })).rejects.toThrow('Product nonexistent not found')\n })\n})\n","typescript",[47,3573,3574,3579,3597,3610,3622,3634,3638,3650,3666,3680,3720,3735,3739,3743,3764,3787,3797,3813,3817,3821,3841,3854,3888,3892,3896,3915,3928,3936,3950,3966,3970],{"__ignoreMap":62},[66,3575,3576],{"class":68,"line":69},[66,3577,3578],{"class":582},"// Test for CreateOrderUseCase — no database, no HTTP, no framework\n",[66,3580,3581,3584,3586,3589,3592,3595],{"class":68,"line":80},[66,3582,3583],{"class":219},"describe",[66,3585,108],{"class":76},[66,3587,3588],{"class":90},"'CreateOrderUseCase'",[66,3590,3591],{"class":76},", () ",[66,3593,3594],{"class":72},"=>",[66,3596,77],{"class":76},[66,3598,3599,3602,3605,3607],{"class":68,"line":97},[66,3600,3601],{"class":72}," let",[66,3603,3604],{"class":76}," orderRepo",[66,3606,152],{"class":72},[66,3608,3609],{"class":219}," InMemoryOrderRepository\n",[66,3611,3612,3614,3617,3619],{"class":68,"line":128},[66,3613,3601],{"class":72},[66,3615,3616],{"class":76}," productRepo",[66,3618,152],{"class":72},[66,3620,3621],{"class":219}," InMemoryProductRepository\n",[66,3623,3624,3626,3629,3631],{"class":68,"line":141},[66,3625,3601],{"class":72},[66,3627,3628],{"class":76}," useCase",[66,3630,152],{"class":72},[66,3632,3633],{"class":219}," CreateOrderUseCase\n",[66,3635,3636],{"class":68,"line":259},[66,3637,459],{"emptyLinePlaceholder":458},[66,3639,3640,3643,3646,3648],{"class":68,"line":265},[66,3641,3642],{"class":219}," beforeEach",[66,3644,3645],{"class":76},"(() ",[66,3647,3594],{"class":72},[66,3649,77],{"class":76},[66,3651,3652,3655,3657,3660,3663],{"class":68,"line":450},[66,3653,3654],{"class":76}," orderRepo ",[66,3656,223],{"class":72},[66,3658,3659],{"class":72}," new",[66,3661,3662],{"class":219}," InMemoryOrderRepository",[66,3664,3665],{"class":76},"()\n",[66,3667,3668,3671,3673,3675,3678],{"class":68,"line":455},[66,3669,3670],{"class":76}," productRepo ",[66,3672,223],{"class":72},[66,3674,3659],{"class":72},[66,3676,3677],{"class":219}," InMemoryProductRepository",[66,3679,3665],{"class":76},[66,3681,3682,3685,3688,3690,3693,3696,3698,3701,3703,3706,3709,3712,3714,3717],{"class":68,"line":462},[66,3683,3684],{"class":76}," productRepo.",[66,3686,3687],{"class":219},"add",[66,3689,108],{"class":76},[66,3691,3692],{"class":72},"new",[66,3694,3695],{"class":219}," Product",[66,3697,108],{"class":76},[66,3699,3700],{"class":90},"'prod-1'",[66,3702,479],{"class":76},[66,3704,3705],{"class":90},"'Widget'",[66,3707,3708],{"class":76},", Money.",[66,3710,3711],{"class":219},"of",[66,3713,108],{"class":76},[66,3715,3716],{"class":83},"29.99",[66,3718,3719],{"class":76},")))\n",[66,3721,3722,3725,3727,3729,3732],{"class":68,"line":470},[66,3723,3724],{"class":76}," useCase ",[66,3726,223],{"class":72},[66,3728,3659],{"class":72},[66,3730,3731],{"class":219}," CreateOrderUseCase",[66,3733,3734],{"class":76},"(orderRepo, productRepo)\n",[66,3736,3737],{"class":68,"line":491},[66,3738,1149],{"class":76},[66,3740,3741],{"class":68,"line":1175},[66,3742,459],{"emptyLinePlaceholder":458},[66,3744,3745,3748,3750,3753,3755,3757,3760,3762],{"class":68,"line":1183},[66,3746,3747],{"class":219}," it",[66,3749,108],{"class":76},[66,3751,3752],{"class":90},"'creates an order with valid items'",[66,3754,479],{"class":76},[66,3756,1291],{"class":72},[66,3758,3759],{"class":76}," () ",[66,3761,3594],{"class":72},[66,3763,77],{"class":76},[66,3765,3766,3769,3772,3775,3778,3781,3784],{"class":68,"line":1191},[66,3767,3768],{"class":72}," const",[66,3770,3771],{"class":83}," orderId",[66,3773,3774],{"class":72}," =",[66,3776,3777],{"class":72}," await",[66,3779,3780],{"class":76}," useCase.",[66,3782,3783],{"class":219},"execute",[66,3785,3786],{"class":76},"({\n",[66,3788,3789,3792,3795],{"class":68,"line":1846},[66,3790,3791],{"class":76}," customerId: ",[66,3793,3794],{"class":90},"'cust-1'",[66,3796,1127],{"class":76},[66,3798,3799,3802,3804,3807,3810],{"class":68,"line":1854},[66,3800,3801],{"class":76}," items: [{ productId: ",[66,3803,3700],{"class":90},[66,3805,3806],{"class":76},", quantity: ",[66,3808,3809],{"class":83},"2",[66,3811,3812],{"class":76}," }]\n",[66,3814,3815],{"class":68,"line":1865},[66,3816,1149],{"class":76},[66,3818,3819],{"class":68,"line":1876},[66,3820,459],{"emptyLinePlaceholder":458},[66,3822,3823,3825,3828,3830,3832,3835,3838],{"class":68,"line":1887},[66,3824,3768],{"class":72},[66,3826,3827],{"class":83}," order",[66,3829,3774],{"class":72},[66,3831,3777],{"class":72},[66,3833,3834],{"class":76}," orderRepo.",[66,3836,3837],{"class":219},"findById",[66,3839,3840],{"class":76},"(orderId)\n",[66,3842,3843,3846,3849,3852],{"class":68,"line":1893},[66,3844,3845],{"class":219}," expect",[66,3847,3848],{"class":76},"(order).",[66,3850,3851],{"class":219},"toBeDefined",[66,3853,3665],{"class":76},[66,3855,3856,3858,3861,3864,3866,3869,3872,3875,3878,3880,3882,3885],{"class":68,"line":1899},[66,3857,3845],{"class":219},[66,3859,3860],{"class":76},"(order",[66,3862,3863],{"class":72},"!",[66,3865,1138],{"class":76},[66,3867,3868],{"class":219},"getTotal",[66,3870,3871],{"class":76},"()).",[66,3873,3874],{"class":219},"toEqual",[66,3876,3877],{"class":76},"(Money.",[66,3879,3711],{"class":219},[66,3881,108],{"class":76},[66,3883,3884],{"class":83},"59.98",[66,3886,3887],{"class":76},"))\n",[66,3889,3890],{"class":68,"line":1905},[66,3891,1149],{"class":76},[66,3893,3894],{"class":68,"line":1911},[66,3895,459],{"emptyLinePlaceholder":458},[66,3897,3898,3900,3902,3905,3907,3909,3911,3913],{"class":68,"line":1917},[66,3899,3747],{"class":219},[66,3901,108],{"class":76},[66,3903,3904],{"class":90},"'throws when product does not exist'",[66,3906,479],{"class":76},[66,3908,1291],{"class":72},[66,3910,3759],{"class":76},[66,3912,3594],{"class":72},[66,3914,77],{"class":76},[66,3916,3917,3919,3921,3924,3926],{"class":68,"line":1923},[66,3918,3777],{"class":72},[66,3920,3845],{"class":219},[66,3922,3923],{"class":76},"(useCase.",[66,3925,3783],{"class":219},[66,3927,3786],{"class":76},[66,3929,3930,3932,3934],{"class":68,"line":1928},[66,3931,3791],{"class":76},[66,3933,3794],{"class":90},[66,3935,1127],{"class":76},[66,3937,3938,3940,3943,3945,3948],{"class":68,"line":1934},[66,3939,3801],{"class":76},[66,3941,3942],{"class":90},"'nonexistent'",[66,3944,3806],{"class":76},[66,3946,3947],{"class":83},"1",[66,3949,3812],{"class":76},[66,3951,3952,3955,3958,3960,3963],{"class":68,"line":1940},[66,3953,3954],{"class":76}," })).rejects.",[66,3956,3957],{"class":219},"toThrow",[66,3959,108],{"class":76},[66,3961,3962],{"class":90},"'Product nonexistent not found'",[66,3964,3965],{"class":76},")\n",[66,3967,3968],{"class":68,"line":1945},[66,3969,1149],{"class":76},[66,3971,3972],{"class":68,"line":1951},[66,3973,3974],{"class":76},"})\n",[20,3976,3977],{},"These tests run in milliseconds. They're deterministic. They test business rules in isolation. When they fail, the failure is in the business logic — not in database connectivity or HTTP parsing.",[20,3979,3980],{},"The adapter tests are separate:",[304,3982,3983,3988,3993],{},[307,3984,3985,3987],{},[47,3986,3442],{}," integration tests run against a real database (or test container)",[307,3989,3990,3992],{},[47,3991,3483],{}," tests run with a real HTTP server",[307,3994,3995],{},"These tests are slower and fewer in number, focused on verifying the integration layer",[20,3997,3998],{},"This separation is one of the most valuable structural properties of hexagonal architecture.",[40,4000],{},[15,4002,4004],{"id":4003},"handling-cross-cutting-concerns","Handling Cross-Cutting Concerns",[20,4006,4007],{},"Hexagonal architecture often raises a question: where does transaction management go? Authorization? Logging?",[20,4009,4010,4013],{},[26,4011,4012],{},"Transactions"," typically belong in the use case or in a decorator/wrapper around the secondary ports. The use case orchestrates a business operation; the transaction scope wraps that operation. Some teams use a Unit of Work pattern that encapsulates the transaction management outside the individual repository calls.",[20,4015,4016,4019],{},[26,4017,4018],{},"Authorization"," belongs at the primary port level or as a use case guard. The controller can verify coarse-grained permissions before invoking the use case. The use case can enforce fine-grained business rules.",[20,4021,4022,4025],{},[26,4023,4024],{},"Logging and observability"," are typically cross-cutting and implemented via decorators that wrap port implementations, or through domain events that observers can subscribe to.",[40,4027],{},[15,4029,4031],{"id":4030},"when-hexagonal-architecture-pays-off","When Hexagonal Architecture Pays Off",[20,4033,4034],{},"The overhead of hexagonal architecture — defining ports, implementing multiple adapters, managing the composition root — pays off when:",[304,4036,4037,4040,4043,4046],{},[307,4038,4039],{},"Your domain has complex business rules that need thorough unit testing in isolation",[307,4041,4042],{},"The system will need to support multiple delivery mechanisms (HTTP API, GraphQL, CLI, event consumer)",[307,4044,4045],{},"Infrastructure choices might change (switching databases, adding a cache, changing payment providers)",[307,4047,4048],{},"The team has the discipline to enforce the dependency rule consistently",[20,4050,4051],{},"It is overkill for simple CRUD applications with minimal business logic. A system that mostly stores and retrieves data without meaningful business rules has little domain logic to protect and little benefit from isolating it.",[20,4053,4054],{},"Apply the pattern where it earns its complexity. Skip it where it doesn't.",[40,4056],{},[20,4058,4059],{},"Hexagonal architecture is one of my default starting points for systems with meaningful business logic. The testability it provides and the freedom from infrastructure lock-in are real, lasting benefits.",[40,4061],{},[20,4063,4064,4065],{},"If you're implementing hexagonal architecture or evaluating whether it fits your domain, ",[690,4066,4068],{"href":692,"rel":4067},[694],"I'd be glad to work through the specifics with you.",[40,4070],{},[15,4072,702],{"id":701},[304,4074,4075,4081,4087,4093],{},[307,4076,4077],{},[690,4078,4080],{"href":4079},"/blog/clean-architecture-guide","Clean Architecture in Practice (Beyond the Circles Diagram)",[307,4082,4083],{},[690,4084,4086],{"href":4085},"/blog/architecture-decision-records","Architecture Decision Records: Why You Need Them and How to Write Them",[307,4088,4089],{},[690,4090,4092],{"href":4091},"/blog/event-driven-architecture-guide","Event-Driven Architecture: When It's the Right Call",[307,4094,4095],{},[690,4096,4098],{"href":4097},"/blog/software-architecture-patterns","Software Architecture Patterns Every Architect Should Know",[730,4100,4101],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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":62,"searchDepth":97,"depth":97,"links":4103},[4104,4105,4106,4107,4108,4109,4110,4111],{"id":3363,"depth":80,"text":3364},{"id":3378,"depth":80,"text":3379},{"id":3421,"depth":80,"text":3422},{"id":3508,"depth":80,"text":3509},{"id":3526,"depth":80,"text":3527},{"id":4003,"depth":80,"text":4004},{"id":4030,"depth":80,"text":4031},{"id":701,"depth":80,"text":702},"Architecture","Hexagonal architecture (ports and adapters) puts your domain at the center and keeps infrastructure at the edge. Here's how it works, why testability improves dramatically, and how to implement it practically.",[4115,4116,4117,4118,4119],"hexagonal architecture ports and adapters","hexagonal architecture","ports and adapters pattern","alistair cockburn hexagonal","hexagonal architecture implementation",{},"/blog/hexagonal-architecture-guide",{"title":3357,"description":4113},"blog/hexagonal-architecture-guide",[4125,4126,4127,4128],"Hexagonal Architecture","Ports and Adapters","Software Architecture","Clean Architecture","aYpWMnsOPkNz-JnUShLGBzkXUA6-Ug7gT9MRGdTPUzE",{"id":4131,"title":4132,"author":4133,"body":4135,"category":4459,"date":746,"description":4460,"extension":748,"featured":749,"image":750,"keywords":4461,"meta":4469,"navigation":458,"path":4470,"readTime":462,"seo":4471,"stem":4472,"tags":4473,"__hash__":4478},"blog/blog/highland-clearances-clan-ross-diaspora.md","The Highland Clearances and Clan Ross: How a People Were Scattered",{"name":9,"bio":4134},"Author of The Forge of Tongues — 22,000 Years of Migration, Mutation, and Memory",{"type":12,"value":4136,"toc":4445},[4137,4141,4144,4147,4153,4156,4159,4161,4165,4168,4171,4182,4188,4194,4197,4199,4203,4206,4212,4218,4224,4234,4236,4240,4243,4247,4253,4264,4270,4274,4285,4292,4296,4313,4316,4318,4322,4325,4328,4331,4333,4337,4347,4350,4352,4356,4359,4362,4404,4407,4409,4413,4439],[15,4138,4140],{"id":4139},"the-scattering","The Scattering",[20,4142,4143],{},"The Ross clan's territory — Ross-shire in the northern Scottish Highlands — was settled by the ancestors of the clan for at least 800 years of documented history and, by the genetic record, for very much longer. The earls of Ross held the territory from 1215 until the earldom lapsed in the late fifteenth century. The chiefs of Clan Ross maintained connection to the land through centuries of change: the Reformation, the Jacobite risings, the transformation of the Highland economy.",[20,4145,4146],{},"Then, in the late eighteenth and early nineteenth centuries, the land itself was effectively cleared of its people.",[20,4148,45,4149,4152],{},[26,4150,4151],{},"Highland Clearances"," — the forced displacement of Highland and Island communities from their agricultural lands to make way for commercial sheep farming and deer forests — was one of the most traumatic events in Scottish history. It was not a sudden catastrophe but a sustained process, extending from the 1760s through the 1880s, driven by the economics of post-Union Britain and carried out by landlords who often included the traditional clan chiefs themselves.",[20,4154,4155],{},"In Ross-shire, the Clearances were among the worst in the Highlands. The population of the interior glens — communities that had farmed the same land for generations — was displaced to the coastal margins or removed entirely, with evictions often carried out with casual brutality. Tens of thousands of people left Ross-shire within a century, many of them involuntarily.",[20,4157,4158],{},"The result was a diaspora spread across the English-speaking world. The scattering of the Ross name from Ross-shire to Nova Scotia, North Carolina, and beyond.",[40,4160],{},[15,4162,4164],{"id":4163},"the-economic-logic-of-displacement","The Economic Logic of Displacement",[20,4166,4167],{},"The Clearances were driven by a specific economic calculation: sheep are more profitable than people.",[20,4169,4170],{},"After the Jacobite defeat at Culloden in 1746, the British government systematically dismantled the Highland clan system — banning Highland dress, disarming the clans, abolishing the hereditary jurisdictions that gave clan chiefs legal authority over their territories. What remained to the chiefs was their role as landlords. And as landlords, they faced the same economic pressures as any other British property-owners in the industrial era.",[20,4172,4173,4176,4177,4181],{},[26,4174,4175],{},"The sheep factor:"," Blackface and Cheviot sheep could graze the interior Highland glens far more profitably than the traditional mixed farming communities of small tenants (",[4178,4179,4180],"em",{},"crofters","). A single sheep run on a cleared estate could generate rental income many times what the same land produced under traditional agriculture.",[20,4183,4184,4187],{},[26,4185,4186],{},"The improvement ideology:"," The language of the Clearances was often framed in terms of \"improvement\" — modernising backward Highland communities, replacing subsistence agriculture with productive commercial farming. The ideology of agricultural improvement provided moral cover for what was, in practice, mass eviction.",[20,4189,4190,4193],{},[26,4191,4192],{},"The collapse of the kelp industry:"," In the early nineteenth century, the kelp industry — harvesting and burning seaweed for alkaline ash used in glass and soap manufacture — employed thousands of Highlanders. When synthetic alternatives arrived in the 1820s, the industry collapsed overnight, removing the economic justification landlords had used for keeping large coastal populations.",[20,4195,4196],{},"The timing of the Ross-shire Clearances coincided with these economic shifts. The great inland glens — Strathconon, Glenstrathfarrar, Strathbran — were cleared through the first half of the nineteenth century. The communities that had farmed them were pushed to the coast or onto emigrant ships.",[40,4198],{},[15,4200,4202],{"id":4201},"the-ross-shire-clearances-specific-events","The Ross-shire Clearances: Specific Events",[20,4204,4205],{},"The Clearances in Ross-shire were extensive and, in several cases, notorious.",[20,4207,4208,4211],{},[26,4209,4210],{},"Strathconon"," — the glen running west from Marybank toward the western Highlands — was cleared in multiple waves from the 1840s through the 1860s. Thousands of people were displaced from the interior to make way for sheep and deer. The glen that once supported a significant agricultural population became, within a generation, a sporting estate.",[20,4213,4214,4217],{},[26,4215,4216],{},"The Glens of the Black Isle"," — the peninsula between the Cromarty Firth and the Beauly Firth — saw displacement of small tenants as larger farms consolidated and the improving landlords reorganised their estates.",[20,4219,4220,4223],{},[26,4221,4222],{},"Greenyards, Strathcarron"," (1854) — one of the most notorious incidents of the Ross-shire Clearances. Sheriff officers attempting to serve eviction notices were met with resistance from local women. A police force was called in, and the confrontation turned violent. The Greenyards incident became a cause célèbre in the anti-Clearance press.",[20,4225,4226,4229,4230,4233],{},[26,4227,4228],{},"The Leckmelm Clearance"," (1879–1880) — a farm on Loch Broom, where a new landlord evicted all the crofting tenants on arrival, demolished their houses to prevent reoccupation, and converted the land to a private estate. The incident contributed to the political agitation that eventually produced the ",[26,4231,4232],{},"Crofters' Holdings (Scotland) Act 1886"," — the first significant legal protection for Highland tenants.",[40,4235],{},[15,4237,4239],{"id":4238},"where-the-ross-diaspora-went","Where the Ross Diaspora Went",[20,4241,4242],{},"The cleared communities left for destinations across the English-speaking world, driven by the dual pressures of eviction and economic destitution.",[1711,4244,4246],{"id":4245},"canada-nova-scotia-and-cape-breton","Canada: Nova Scotia and Cape Breton",[20,4248,4249,4252],{},[26,4250,4251],{},"Nova Scotia"," — literally \"New Scotland\" — was the primary destination for many Highland emigrants from the late eighteenth century onward. The province was settled with enough Highland Scots that Scottish Gaelic was spoken there until the twentieth century. Cape Breton Island, the northern part of Nova Scotia, received particularly large numbers of Highland emigrants, including many from Ross-shire.",[20,4254,4255,4256,4263],{},"The Ross surname is common in Cape Breton. The communities they established maintained Gaelic culture — Gaelic song, Gaelic stories, Gaelic worship — long after it had declined in the homeland. The ",[26,4257,4258],{},[690,4259,4262],{"href":4260,"rel":4261},"https://www.gaeliccollege.edu",[694],"Gaelic College of Celtic Arts and Crafts"," in St. Ann's Bay, Cape Breton, was founded in 1938 and continues to maintain Highland traditions.",[20,4265,4266,4269],{},[26,4267,4268],{},"Ontario and Prince Edward Island"," also received significant Ross-shire emigrant communities during the clearance era.",[1711,4271,4273],{"id":4272},"united-states-the-earlier-wave","United States: The Earlier Wave",[20,4275,4276,4277,4280,4281,4284],{},"The American emigration preceded the mass Clearances. ",[26,4278,4279],{},"North Carolina's Cape Fear Valley"," — settled by Highland Scots from the 1730s onward — included Ross families who emigrated before the American Revolution. The ",[26,4282,4283],{},"Flora MacDonald connection"," — the woman famous for helping Bonnie Prince Charlie escape after Culloden who later emigrated to North Carolina — illustrates the scale of Highland emigration to the American south in the mid-18th century.",[20,4286,4287,4288,4291],{},"After the Revolution, the Great Lakes region, the Prairie states, and eventually every part of the United States received Highland Scottish emigrants. The Ross surname spread with the emigration. Today, ",[26,4289,4290],{},"Ross"," is among the more common Scottish-origin surnames in the American South and Midwest.",[1711,4293,4295],{"id":4294},"australia","Australia",[20,4297,4298,4301,4302,4305,4306,1376,4309,4312],{},[26,4299,4300],{},"Tasmania"," (Van Diemen's Land) and ",[26,4303,4304],{},"Victoria"," received Highland emigrants, some displaced by the Clearances, some transported as convicts for offences connected to the resistance to eviction. ",[26,4307,4308],{},"South Australia",[26,4310,4311],{},"New Zealand"," also received Highland communities.",[20,4314,4315],{},"The 1851 gold rush brought Scottish emigrants to Victoria in large numbers. The Highland communities of Australia maintained clan associations and cultural connections to the homeland through the nineteenth and twentieth centuries.",[40,4317],{},[15,4319,4321],{"id":4320},"the-irony-of-the-ross-chiefs","The Irony of the Ross Chiefs",[20,4323,4324],{},"Among the most painful aspects of the Clearances in Ross-shire is the role of the Ross chiefs themselves. By the nineteenth century, the Balnagown estate — the ancestral seat of the Ross chiefs, held by the clan from the medieval period — had passed out of direct Ross family control in 1672. Subsequent proprietors of the Balnagown lands included various improving landlords who carried out their own clearances without the connection to the clan tradition that had shaped the earlier chiefs' relationship with their tenants.",[20,4326,4327],{},"This is the brutal arithmetic of the Clearances: the ideology of improvement, the cash economy of post-Union Britain, and the legal transformation of clan chiefs into mere landlords combined to sever the relationship between the great families and the communities they had once led in a different kind of compact.",[20,4329,4330],{},"The earldom of Ross had lapsed in 1476. The last earl — John of Islay, who forfeited the earldom for treasonous dealings with England — was also the last holder of the title. Subsequent Ross chiefs held their position through clan tradition rather than legal title, and by the nineteenth century, that tradition had been substantially eroded by the commercial pressures of the era.",[40,4332],{},[15,4334,4336],{"id":4335},"the-crofters-holdings-act-and-after","The Crofters' Holdings Act and After",[20,4338,4339,4340,4343,4344,4346],{},"The political resistance to the Clearances eventually produced results. The ",[26,4341,4342],{},"Napier Commission"," (1883) — established by Gladstone's government to investigate the condition of crofters — heard extensive testimony about the conditions in the Highlands, including Ross-shire. Its report led directly to the ",[26,4345,4232],{},", which gave crofters security of tenure, fair rent, and the right to pass on their holdings to family members.",[20,4348,4349],{},"The Act did not reverse the Clearances — the people who had been removed were gone, and their descendants were in Cape Breton, North Carolina, and Melbourne. But it ended the era of arbitrary eviction that had defined the clearance period and gave the remaining Highland communities a degree of protection they had never previously possessed under Scottish law.",[40,4351],{},[15,4353,4355],{"id":4354},"tracing-your-ross-clearance-heritage","Tracing Your Ross Clearance Heritage",[20,4357,4358],{},"If your family carries the Ross name and emigrated from Scotland in the 1800s, there is a reasonable probability that the emigration was connected — directly or indirectly — to the Clearances.",[20,4360,4361],{},"Resources for tracing Ross-shire ancestry include:",[304,4363,4364,4374,4384,4394],{},[307,4365,4366,4373],{},[26,4367,4368],{},[690,4369,4372],{"href":4370,"rel":4371},"https://www.scotlandspeople.gov.uk",[694],"ScotlandsPeople"," — digitised Scottish civil records (from 1855), church records (OPRs from before 1855), and census records",[307,4375,4376,4383],{},[26,4377,4378],{},[690,4379,4382],{"href":4380,"rel":4381},"https://www.ambaile.org.uk",[694],"Am Baile"," — Highland history and culture digital archive with Ross-shire material",[307,4385,4386,4393],{},[26,4387,4388],{},[690,4389,4392],{"href":4390,"rel":4391},"https://www.highlandarchivescentre.org.uk",[694],"Highland Archive Centre, Inverness"," — holds county records, estate papers, and genealogical collections for Ross-shire and surrounding areas",[307,4395,4396,4403],{},[26,4397,4398],{},[690,4399,4402],{"href":4400,"rel":4401},"https://www.familytreedna.com/groups/ross/about",[694],"FamilyTreeDNA Ross Surname Project"," — aggregates Y-DNA results from Ross men worldwide; useful for identifying which genetic cluster your line belongs to",[20,4405,4406],{},"The DNA can take you back before the records run out. The clearance-era emigrants left a genetic legacy in Nova Scotia and the American South that can be connected to the Highland Ross lines through Y-chromosome testing.",[40,4408],{},[15,4410,4412],{"id":4411},"related-articles","Related Articles",[304,4414,4415,4421,4427,4433],{},[307,4416,4417],{},[690,4418,4420],{"href":4419},"/blog/fearchar-mac-an-t-sagairt-earl-ross","Fearchar mac an t-Sagairt: The First Earl of Ross",[307,4422,4423],{},[690,4424,4426],{"href":4425},"/blog/ross-surname-origin-meaning","The Ross Surname: Scottish Origins, Meaning, and Where the Name Came From",[307,4428,4429],{},[690,4430,4432],{"href":4431},"/blog/applecross-obeolans-monks-dynasty","The O'Beolans of Applecross: The Monks Who Became a Dynasty",[307,4434,4435],{},[690,4436,4438],{"href":4437},"/blog/what-is-genetic-genealogy","What Is Genetic Genealogy? A Beginner's Guide to DNA Ancestry Research",[20,4440,4441],{},[690,4442,4444],{"href":4443},"/book","Read the full story of Clan Ross from Applecross to the diaspora in The Forge of Tongues: 22,000 Years of Migration, Mutation, and Memory.",{"title":62,"searchDepth":97,"depth":97,"links":4446},[4447,4448,4449,4450,4455,4456,4457,4458],{"id":4139,"depth":80,"text":4140},{"id":4163,"depth":80,"text":4164},{"id":4201,"depth":80,"text":4202},{"id":4238,"depth":80,"text":4239,"children":4451},[4452,4453,4454],{"id":4245,"depth":97,"text":4246},{"id":4272,"depth":97,"text":4273},{"id":4294,"depth":97,"text":4295},{"id":4320,"depth":80,"text":4321},{"id":4335,"depth":80,"text":4336},{"id":4354,"depth":80,"text":4355},{"id":4411,"depth":80,"text":4412},"Heritage","The Highland Clearances of the 18th and 19th centuries were particularly devastating in Ross-shire. Tens of thousands of people were displaced from their ancestral lands to make way for sheep. Here's the story of how the Ross diaspora was created — and where they went.",[4462,4463,4464,4465,4466,4467,4468],"highland clearances clan ross","clan ross diaspora","highland clearances ross-shire","scottish emigration canada","clan ross america","clan ross canada","nova scotia scottish settlers",{},"/blog/highland-clearances-clan-ross-diaspora",{"title":4132,"description":4460},"blog/highland-clearances-clan-ross-diaspora",[4151,4474,4475,4476,4251,4477],"Clan Ross","Scottish Diaspora","Ross-shire History","Scottish Emigration","5buaENqyrH0NnrK9jQaLfvvg48q9Fp6epq_W8H4rqKI",{"id":4480,"title":943,"author":4481,"body":4482,"category":972,"date":746,"description":4703,"extension":748,"featured":749,"image":750,"keywords":4704,"meta":4707,"navigation":458,"path":942,"readTime":265,"seo":4708,"stem":4709,"tags":4710,"__hash__":4711},"blog/blog/hiring-software-development-company.md",{"name":9,"bio":10},{"type":12,"value":4483,"toc":4694},[4484,4488,4491,4494,4496,4500,4503,4506,4509,4523,4526,4528,4532,4535,4538,4544,4550,4556,4562,4568,4570,4574,4580,4586,4592,4598,4600,4604,4610,4616,4622,4628,4630,4634,4637,4643,4649,4655,4661,4664,4666,4672,4674,4676],[15,4485,4487],{"id":4486},"why-this-decision-is-hard-to-undo","Why This Decision Is Hard to Undo",[20,4489,4490],{},"Hiring a software development company is one of those decisions that looks easy from the outside and turns out to be consequential in ways you didn't anticipate. Once a team has been working on your codebase for six months, switching costs are enormous — not just financially, but technically. The code they've written is now your code. Their architectural decisions are now your architectural decisions. Walking away means starting over or inheriting whatever they built.",[20,4492,4493],{},"This is why the front-end of the process — how you evaluate, interview, and select a development partner — matters as much as any other part of the project. A mediocre vetting process produces a mediocre partner, and you'll be living with the consequences for years.",[40,4495],{},[15,4497,4499],{"id":4498},"the-first-filter-what-theyve-actually-built","The First Filter: What They've Actually Built",[20,4501,4502],{},"The portfolio is the most important artifact a development company can show you. But most portfolios are marketing, not evidence. Screenshots of polished UIs, logos of well-known clients, and vague statements about \"scalable solutions\" tell you almost nothing about whether they can build what you need.",[20,4504,4505],{},"What you want to see is work that is technically, industry, and scale-similar to your project. If you're building a multi-tenant B2B SaaS application, you want to see another multi-tenant B2B SaaS application in their portfolio — not a consumer mobile app and a marketing website.",[20,4507,4508],{},"When reviewing their work, ask:",[304,4510,4511,4514,4517,4520],{},[307,4512,4513],{},"What was the tech stack?",[307,4515,4516],{},"How many concurrent users does the system handle?",[307,4518,4519],{},"Who managed the architecture — was there a lead architect, or was it a junior team?",[307,4521,4522],{},"Is the client still using this software? Why or why not?",[20,4524,4525],{},"That last question is underrated. If a client commissioned a custom system and abandoned it within 18 months, that's a data point.",[40,4527],{},[15,4529,4531],{"id":4530},"reference-checks-that-are-actually-useful","Reference Checks That Are Actually Useful",[20,4533,4534],{},"Most companies offer references, and most people treat reference checks as a formality. That's a mistake. The reference call is your best source of ground truth.",[20,4536,4537],{},"Ask for references from projects that are most similar to yours. Then ask the reference these specific questions:",[20,4539,4540,4543],{},[26,4541,4542],{},"Did the final cost come in close to the original estimate?"," If it was more than 30% over, ask why. Sometimes the reasons are legitimate (scope changes, changing requirements). Sometimes they're not.",[20,4545,4546,4549],{},[26,4547,4548],{},"How did they communicate when something went wrong?"," Every project has problems. The question is how the company handles them — proactively and transparently, or quietly until it becomes unavoidable.",[20,4551,4552,4555],{},[26,4553,4554],{},"Did they meet their delivery commitments?"," Not just the final date, but intermediate milestones. A team that consistently misses milestones by small amounts is running a different kind of schedule than they're telling you.",[20,4557,4558,4561],{},[26,4559,4560],{},"Would you hire them again?"," Listen for hesitation, qualifications, or deflections. \"They did a good job overall\" is not the same as \"absolutely, without question.\"",[20,4563,4564,4567],{},[26,4565,4566],{},"What would you do differently if you were starting the engagement over?"," This question gets you the most honest information, because it frames the criticism as self-reflection rather than complaint.",[40,4569],{},[15,4571,4573],{"id":4572},"green-flags-in-the-evaluation-process","Green Flags in the Evaluation Process",[20,4575,4576,4579],{},[26,4577,4578],{},"They push back on your requirements."," A development company that agrees with everything you say and produces a quote in 48 hours without asking hard questions is not doing their job. The best shops will challenge your assumptions, identify requirements gaps, and flag technical risks before they take your money. Pushback is a sign of expertise, not difficult clients.",[20,4581,4582,4585],{},[26,4583,4584],{},"They propose a discovery phase."," Serious development companies don't quote a full project from a brief. They propose a paid discovery phase that produces a specification, an architecture recommendation, and a detailed scope document. This is good practice, and it tells you they're committed to understanding your project before they build it.",[20,4587,4588,4591],{},[26,4589,4590],{},"They use version control, CI/CD, and code review as standard practice."," Ask directly: what does your development process look like? How do you manage code? How do you test? What does a deployment look like? Answers that mention Git, automated testing, deployment pipelines, and code review are answers from a team that has basic engineering hygiene. Vague answers about \"our process\" with no specifics are a red flag.",[20,4593,4594,4597],{},[26,4595,4596],{},"They can explain their stack choices."," If they say \"we use React Native for mobile,\" ask why. If they say \"we use PostgreSQL for your database,\" ask why. A team that can articulate the reasoning behind their technology choices has made those choices deliberately. A team that defaults to whatever they always use without thinking about your specific needs is treating your project as a commodity.",[40,4599],{},[15,4601,4603],{"id":4602},"red-flags-that-should-stop-the-conversation","Red Flags That Should Stop the Conversation",[20,4605,4606,4609],{},[26,4607,4608],{},"They can't give you a point of contact who is technically accountable."," Some development companies are fundamentally sales organizations — a polished front-end staffed by account managers who then subcontract the actual work to developers you've never met in places you didn't know about. Ask directly who will be writing the code and who has technical accountability for the project. If you can't get a specific name with a specific role, stop.",[20,4611,4612,4615],{},[26,4613,4614],{},"Their contract transfers no IP until final payment."," A legitimate development contract should transfer intellectual property as work is completed, or at minimum upon payment of each invoice. A contract that retains all IP until the final invoice is paid is holding your software hostage. This is a leverage mechanism, not a standard business practice.",[20,4617,4618,4621],{},[26,4619,4620],{},"They offer an unusually low quote."," Below-market pricing usually means one of three things: they're using offshore junior developers (which may be fine if disclosed, problematic if hidden), they've intentionally underscoped the project and will make it up in change orders, or they're desperate for work for reasons you should understand. Get specifics about staffing and experience levels.",[20,4623,4624,4627],{},[26,4625,4626],{},"They can't show you code."," Not the full codebase of a client project — but some representation of their code quality. A technical test project, open source contributions, or a code sample from a past project with client permission. Teams that produce good code generally aren't defensive about showing it. Teams that produce mediocre code often are.",[40,4629],{},[15,4631,4633],{"id":4632},"structuring-the-contract-for-protection","Structuring the Contract for Protection",[20,4635,4636],{},"Before you sign anything, make sure the contract addresses:",[20,4638,4639,4642],{},[26,4640,4641],{},"Escrow of code."," If the relationship ends, you have immediate access to the full repository.",[20,4644,4645,4648],{},[26,4646,4647],{},"Milestone-based payment."," Pay for completed, accepted work — not for time in advance. Each payment should be tied to a deliverable you've verified and approved.",[20,4650,4651,4654],{},[26,4652,4653],{},"Termination for convenience."," You should be able to end the engagement with reasonable notice (30-60 days is standard) without penalty beyond payment for work completed.",[20,4656,4657,4660],{},[26,4658,4659],{},"Definition of done."," What does \"complete\" mean for each deliverable? Who approves it? What's the process for defects discovered after approval?",[20,4662,4663],{},"Spend money on a lawyer to review this contract. The cost is trivial relative to the value of the project and the cost of a dispute.",[40,4665],{},[20,4667,4668,4669,1138],{},"The hiring decision is where a software project is often won or lost — before a line of code is written. If you'd like help evaluating a development partner or want a second opinion on a proposal you've received, book a call at ",[690,4670,695],{"href":692,"rel":4671},[694],[40,4673],{},[15,4675,702],{"id":701},[304,4677,4678,4682,4686,4690],{},[307,4679,4680],{},[690,4681,765],{"href":978},[307,4683,4684],{},[690,4685,955],{"href":954},[307,4687,4688],{},[690,4689,949],{"href":948},[307,4691,4692],{},[690,4693,961],{"href":960},{"title":62,"searchDepth":97,"depth":97,"links":4695},[4696,4697,4698,4699,4700,4701,4702],{"id":4486,"depth":80,"text":4487},{"id":4498,"depth":80,"text":4499},{"id":4530,"depth":80,"text":4531},{"id":4572,"depth":80,"text":4573},{"id":4602,"depth":80,"text":4603},{"id":4632,"depth":80,"text":4633},{"id":701,"depth":80,"text":702},"Choosing the wrong software development partner is an expensive mistake. Here's the due diligence process I'd run before signing any contract for custom development.",[4705,4706],"hiring software development company","enterprise software development company",{},{"title":943,"description":4703},"blog/hiring-software-development-company",[984,983,982],"eGsQxCVvpYzZu4iLpxLxzC__KPB0Lo5da1BlveQi1yA",{"id":4713,"title":4714,"author":4715,"body":4716,"category":745,"date":746,"description":5463,"extension":748,"featured":749,"image":750,"keywords":5464,"meta":5467,"navigation":458,"path":5468,"readTime":265,"seo":5469,"stem":5470,"tags":5471,"__hash__":5474},"blog/blog/hono-vs-express.md","Hono vs Express: Is the New Kid Worth Switching To?",{"name":9,"bio":10},{"type":12,"value":4717,"toc":5452},[4718,4721,4725,4728,4731,4734,4738,4741,4981,4995,4998,5001,5019,5022,5025,5029,5035,5046,5299,5302,5306,5309,5335,5338,5341,5345,5348,5355,5359,5365,5371,5377,5381,5387,5393,5399,5405,5408,5411,5413,5419,5421,5423,5449],[20,4719,4720],{},"Express has been the default Node.js web framework for over a decade. It is reliable, has a massive ecosystem, and every Node.js developer knows it. Hono arrived more recently with a different set of priorities — performance, edge runtime compatibility, and first-class TypeScript support. After shipping production applications with both, I have clear opinions on when to reach for each.",[15,4722,4724],{"id":4723},"express-what-it-is-and-what-it-is-not","Express: What It Is and What It Is Not",[20,4726,4727],{},"Express is a minimal, unopinionated routing framework. It provides routing, middleware, and request/response handling. That is almost everything. State management, validation, serialization, type safety — all of that is your responsibility or a third-party library's.",[20,4729,4730],{},"This minimalism was a strength in 2011. It is increasingly a liability in 2026. Every Express application reinvents the same wheel: TypeScript integration, input validation, error handling middleware, OpenAPI schema generation. The ecosystem has solutions for all of this, but assembling them requires judgment and the result is not always consistent.",[20,4732,4733],{},"Express also predates the edge runtime era. It depends on Node.js APIs and cannot run in Cloudflare Workers, Deno Deploy, or Bun's HTTP server without shims.",[15,4735,4737],{"id":4736},"hono-the-pitch","Hono: The Pitch",[20,4739,4740],{},"Hono was built to be fast, edge-compatible, and TypeScript-first. The API is familiar if you know Express but the design decisions are consistently better.",[57,4742,4744],{"className":3569,"code":4743,"language":3571,"meta":62,"style":62},"import { Hono } from 'hono'\nimport { zValidator } from '@hono/zod-validator'\nimport { z } from 'zod'\n\nConst app = new Hono()\n\nConst createUserSchema = z.object({\n name: z.string().min(1),\n email: z.string().email(),\n})\n\nApp.post(\n '/users',\n zValidator('json', createUserSchema),\n async (c) => {\n const data = c.req.valid('json') // Typed as CreateUser\n const user = await createUser(data)\n return c.json(user, 201)\n }\n)\n",[47,4745,4746,4758,4770,4782,4786,4800,4804,4819,4840,4855,4859,4863,4874,4881,4894,4912,4936,4953,4972,4977],{"__ignoreMap":62},[66,4747,4748,4750,4753,4755],{"class":68,"line":69},[66,4749,1074],{"class":72},[66,4751,4752],{"class":76}," { Hono } ",[66,4754,1080],{"class":72},[66,4756,4757],{"class":90}," 'hono'\n",[66,4759,4760,4762,4765,4767],{"class":68,"line":80},[66,4761,1074],{"class":72},[66,4763,4764],{"class":76}," { zValidator } ",[66,4766,1080],{"class":72},[66,4768,4769],{"class":90}," '@hono/zod-validator'\n",[66,4771,4772,4774,4777,4779],{"class":68,"line":97},[66,4773,1074],{"class":72},[66,4775,4776],{"class":76}," { z } ",[66,4778,1080],{"class":72},[66,4780,4781],{"class":90}," 'zod'\n",[66,4783,4784],{"class":68,"line":128},[66,4785,459],{"emptyLinePlaceholder":458},[66,4787,4788,4791,4793,4795,4798],{"class":68,"line":141},[66,4789,4790],{"class":76},"Const app ",[66,4792,223],{"class":72},[66,4794,3659],{"class":72},[66,4796,4797],{"class":219}," Hono",[66,4799,3665],{"class":76},[66,4801,4802],{"class":68,"line":259},[66,4803,459],{"emptyLinePlaceholder":458},[66,4805,4806,4809,4811,4814,4817],{"class":68,"line":265},[66,4807,4808],{"class":76},"Const createUserSchema ",[66,4810,223],{"class":72},[66,4812,4813],{"class":76}," z.",[66,4815,4816],{"class":219},"object",[66,4818,3786],{"class":76},[66,4820,4821,4824,4827,4830,4833,4835,4837],{"class":68,"line":450},[66,4822,4823],{"class":76}," name: z.",[66,4825,4826],{"class":219},"string",[66,4828,4829],{"class":76},"().",[66,4831,4832],{"class":219},"min",[66,4834,108],{"class":76},[66,4836,3947],{"class":83},[66,4838,4839],{"class":76},"),\n",[66,4841,4842,4845,4847,4849,4852],{"class":68,"line":455},[66,4843,4844],{"class":76}," email: z.",[66,4846,4826],{"class":219},[66,4848,4829],{"class":76},[66,4850,4851],{"class":219},"email",[66,4853,4854],{"class":76},"(),\n",[66,4856,4857],{"class":68,"line":462},[66,4858,3974],{"class":76},[66,4860,4861],{"class":68,"line":470},[66,4862,459],{"emptyLinePlaceholder":458},[66,4864,4865,4868,4871],{"class":68,"line":491},[66,4866,4867],{"class":76},"App.",[66,4869,4870],{"class":219},"post",[66,4872,4873],{"class":76},"(\n",[66,4875,4876,4879],{"class":68,"line":1175},[66,4877,4878],{"class":90}," '/users'",[66,4880,1127],{"class":76},[66,4882,4883,4886,4888,4891],{"class":68,"line":1183},[66,4884,4885],{"class":219}," zValidator",[66,4887,108],{"class":76},[66,4889,4890],{"class":90},"'json'",[66,4892,4893],{"class":76},", createUserSchema),\n",[66,4895,4896,4899,4902,4906,4908,4910],{"class":68,"line":1191},[66,4897,4898],{"class":72}," async",[66,4900,4901],{"class":76}," (",[66,4903,4905],{"class":4904},"s9osk","c",[66,4907,114],{"class":76},[66,4909,3594],{"class":72},[66,4911,77],{"class":76},[66,4913,4914,4916,4919,4921,4924,4927,4929,4931,4933],{"class":68,"line":1846},[66,4915,3768],{"class":72},[66,4917,4918],{"class":83}," data",[66,4920,3774],{"class":72},[66,4922,4923],{"class":76}," c.req.",[66,4925,4926],{"class":219},"valid",[66,4928,108],{"class":76},[66,4930,4890],{"class":90},[66,4932,114],{"class":76},[66,4934,4935],{"class":582},"// Typed as CreateUser\n",[66,4937,4938,4940,4943,4945,4947,4950],{"class":68,"line":1854},[66,4939,3768],{"class":72},[66,4941,4942],{"class":83}," user",[66,4944,3774],{"class":72},[66,4946,3777],{"class":72},[66,4948,4949],{"class":219}," createUser",[66,4951,4952],{"class":76},"(data)\n",[66,4954,4955,4958,4961,4964,4967,4970],{"class":68,"line":1865},[66,4956,4957],{"class":72}," return",[66,4959,4960],{"class":76}," c.",[66,4962,4963],{"class":219},"json",[66,4965,4966],{"class":76},"(user, ",[66,4968,4969],{"class":83},"201",[66,4971,3965],{"class":76},[66,4973,4974],{"class":68,"line":1876},[66,4975,4976],{"class":76}," }\n",[66,4978,4979],{"class":68,"line":1887},[66,4980,3965],{"class":76},[20,4982,45,4983,4986,4987,4990,4991,4994],{},[47,4984,4985],{},"zValidator"," middleware validates the request body and narrows the type — ",[47,4988,4989],{},"c.req.valid('json')"," returns a value typed according to your Zod schema. No manual validation calls, no ",[47,4992,4993],{},"as"," casts. This integration is genuinely excellent and Express has nothing equivalent built in.",[15,4996,759],{"id":4997},"performance",[20,4999,5000],{},"Hono benchmarks faster than Express for most workloads. The performance difference comes from:",[304,5002,5003,5006,5009,5012],{},[307,5004,5005],{},"No legacy compatibility code",[307,5007,5008],{},"More efficient routing (uses a radix tree)",[307,5010,5011],{},"Less middleware overhead per request",[307,5013,5014,5015,5018],{},"No Node.js ",[47,5016,5017],{},"http"," module abstraction — uses the Web Fetch API natively",[20,5020,5021],{},"Hono on Bun is particularly fast, competitive with Go frameworks for simple HTTP workloads. On Node.js the difference is meaningful but smaller.",[20,5023,5024],{},"For most CRUD APIs, Express's performance is adequate — database queries and network latency dominate. The Hono performance advantage matters more in high-throughput services, real-time applications, or edge deployments where cold start time is measured.",[15,5026,5028],{"id":5027},"typescript-support","TypeScript Support",[20,5030,5031,5032,5034],{},"This is where Hono is most clearly superior. Hono is written in TypeScript and designed around TypeScript. The context object (",[47,5033,4905],{},") is typed, middleware can extend the context type, and the RPC mode provides end-to-end type safety between your Hono backend and your frontend client.",[20,5036,5037,5038,5041,5042,5045],{},"Express with TypeScript requires ",[47,5039,5040],{},"@types/express",", which provides adequate but not great type coverage. Middleware that extends ",[47,5043,5044],{},"req"," requires manual declaration merging. The types are workable but feel bolted on.",[57,5047,5049],{"className":3569,"code":5048,"language":3571,"meta":62,"style":62},"// Hono RPC: type-safe API client\n// In your server:\nconst routes = app\n .get('/users', async (c) => c.json(await getUsers()))\n .post('/users', zValidator('json', createUserSchema), async (c) => {\n const user = await createUser(c.req.valid('json'))\n return c.json(user, 201)\n })\n\nExport type AppType = typeof routes\n\n// In your client (e.g., Nuxt frontend):\nimport { hc } from 'hono/client'\nimport type { AppType } from '../api'\n\nConst client = hc\u003CAppType>('http://localhost:3000')\n\nConst users = await client.users.$get()\n// users is typed according to your API response\n",[47,5050,5051,5056,5061,5074,5114,5147,5170,5184,5188,5192,5211,5215,5220,5232,5246,5250,5273,5277,5294],{"__ignoreMap":62},[66,5052,5053],{"class":68,"line":69},[66,5054,5055],{"class":582},"// Hono RPC: type-safe API client\n",[66,5057,5058],{"class":68,"line":80},[66,5059,5060],{"class":582},"// In your server:\n",[66,5062,5063,5066,5069,5071],{"class":68,"line":97},[66,5064,5065],{"class":72},"const",[66,5067,5068],{"class":83}," routes",[66,5070,3774],{"class":72},[66,5072,5073],{"class":76}," app\n",[66,5075,5076,5079,5082,5084,5087,5089,5091,5093,5095,5097,5099,5101,5103,5105,5108,5111],{"class":68,"line":128},[66,5077,5078],{"class":76}," .",[66,5080,5081],{"class":219},"get",[66,5083,108],{"class":76},[66,5085,5086],{"class":90},"'/users'",[66,5088,479],{"class":76},[66,5090,1291],{"class":72},[66,5092,4901],{"class":76},[66,5094,4905],{"class":4904},[66,5096,114],{"class":76},[66,5098,3594],{"class":72},[66,5100,4960],{"class":76},[66,5102,4963],{"class":219},[66,5104,108],{"class":76},[66,5106,5107],{"class":72},"await",[66,5109,5110],{"class":219}," getUsers",[66,5112,5113],{"class":76},"()))\n",[66,5115,5116,5118,5120,5122,5124,5126,5128,5130,5132,5135,5137,5139,5141,5143,5145],{"class":68,"line":141},[66,5117,5078],{"class":76},[66,5119,4870],{"class":219},[66,5121,108],{"class":76},[66,5123,5086],{"class":90},[66,5125,479],{"class":76},[66,5127,4985],{"class":219},[66,5129,108],{"class":76},[66,5131,4890],{"class":90},[66,5133,5134],{"class":76},", createUserSchema), ",[66,5136,1291],{"class":72},[66,5138,4901],{"class":76},[66,5140,4905],{"class":4904},[66,5142,114],{"class":76},[66,5144,3594],{"class":72},[66,5146,77],{"class":76},[66,5148,5149,5151,5153,5155,5157,5159,5162,5164,5166,5168],{"class":68,"line":259},[66,5150,3768],{"class":72},[66,5152,4942],{"class":83},[66,5154,3774],{"class":72},[66,5156,3777],{"class":72},[66,5158,4949],{"class":219},[66,5160,5161],{"class":76},"(c.req.",[66,5163,4926],{"class":219},[66,5165,108],{"class":76},[66,5167,4890],{"class":90},[66,5169,3887],{"class":76},[66,5171,5172,5174,5176,5178,5180,5182],{"class":68,"line":265},[66,5173,4957],{"class":72},[66,5175,4960],{"class":76},[66,5177,4963],{"class":219},[66,5179,4966],{"class":76},[66,5181,4969],{"class":83},[66,5183,3965],{"class":76},[66,5185,5186],{"class":68,"line":450},[66,5187,1149],{"class":76},[66,5189,5190],{"class":68,"line":455},[66,5191,459],{"emptyLinePlaceholder":458},[66,5193,5194,5197,5200,5203,5205,5208],{"class":68,"line":462},[66,5195,5196],{"class":76},"Export ",[66,5198,5199],{"class":72},"type",[66,5201,5202],{"class":219}," AppType",[66,5204,3774],{"class":72},[66,5206,5207],{"class":72}," typeof",[66,5209,5210],{"class":76}," routes\n",[66,5212,5213],{"class":68,"line":470},[66,5214,459],{"emptyLinePlaceholder":458},[66,5216,5217],{"class":68,"line":491},[66,5218,5219],{"class":582},"// In your client (e.g., Nuxt frontend):\n",[66,5221,5222,5224,5227,5229],{"class":68,"line":1175},[66,5223,1074],{"class":72},[66,5225,5226],{"class":76}," { hc } ",[66,5228,1080],{"class":72},[66,5230,5231],{"class":90}," 'hono/client'\n",[66,5233,5234,5236,5238,5241,5243],{"class":68,"line":1183},[66,5235,1074],{"class":72},[66,5237,251],{"class":72},[66,5239,5240],{"class":76}," { AppType } ",[66,5242,1080],{"class":72},[66,5244,5245],{"class":90}," '../api'\n",[66,5247,5248],{"class":68,"line":1191},[66,5249,459],{"emptyLinePlaceholder":458},[66,5251,5252,5255,5257,5260,5262,5265,5268,5271],{"class":68,"line":1846},[66,5253,5254],{"class":76},"Const client ",[66,5256,223],{"class":72},[66,5258,5259],{"class":219}," hc",[66,5261,210],{"class":76},[66,5263,5264],{"class":219},"AppType",[66,5266,5267],{"class":76},">(",[66,5269,5270],{"class":90},"'http://localhost:3000'",[66,5272,3965],{"class":76},[66,5274,5275],{"class":68,"line":1854},[66,5276,459],{"emptyLinePlaceholder":458},[66,5278,5279,5282,5284,5286,5289,5292],{"class":68,"line":1865},[66,5280,5281],{"class":76},"Const users ",[66,5283,223],{"class":72},[66,5285,3777],{"class":72},[66,5287,5288],{"class":76}," client.users.",[66,5290,5291],{"class":219},"$get",[66,5293,3665],{"class":76},[66,5295,5296],{"class":68,"line":1876},[66,5297,5298],{"class":582},"// users is typed according to your API response\n",[20,5300,5301],{},"This RPC pattern is compelling for monorepo setups where frontend and backend share a codebase. The alternative — maintaining separate API type definitions — is tedious and prone to drift.",[15,5303,5305],{"id":5304},"edge-compatibility","Edge Compatibility",[20,5307,5308],{},"Hono runs on:",[304,5310,5311,5314,5317,5320,5323,5326,5329,5332],{},[307,5312,5313],{},"Node.js",[307,5315,5316],{},"Cloudflare Workers",[307,5318,5319],{},"Cloudflare Pages Functions",[307,5321,5322],{},"Vercel Edge Functions",[307,5324,5325],{},"Netlify Edge Functions",[307,5327,5328],{},"Deno",[307,5330,5331],{},"Bun",[307,5333,5334],{},"AWS Lambda",[20,5336,5337],{},"The same application code, with adapter-specific entry points, deploys to any of these environments. This is genuinely useful for services that need global distribution or mixed deployment targets.",[20,5339,5340],{},"Express does not run in edge environments. This is not a solvable problem with shims — Edge runtimes do not have the Node.js APIs that Express depends on.",[15,5342,5344],{"id":5343},"middleware-ecosystem","Middleware Ecosystem",[20,5346,5347],{},"Express has ten years of middleware packages. Authentication, rate limiting, logging, compression, CORS, body parsing — the ecosystem is comprehensive. Hono's middleware library is growing rapidly and covers most common needs, but you may occasionally find that a specific library you want only has an Express adapter.",[20,5349,5350,5351,5354],{},"This gap is closing. For most applications, Hono's built-in middleware and the ",[47,5352,5353],{},"@hono/"," namespace packages cover everything needed. But if you depend on a specific Express middleware that has not been ported, that is a real consideration.",[15,5356,5358],{"id":5357},"when-to-use-express","When to Use Express",[20,5360,5361,5364],{},[26,5362,5363],{},"You are working on an existing Express application."," Migrating a working application to Hono provides incremental benefit that may not justify the disruption. Hono shines on new projects.",[20,5366,5367,5370],{},[26,5368,5369],{},"Your team knows Express deeply and needs to move fast."," Familiarity has real value. If your team can ship features in Express quickly, and edge deployment is not a requirement, Express is a reasonable choice.",[20,5372,5373,5376],{},[26,5374,5375],{},"You need a specific Express middleware."," OAuth strategies via Passport, specific authentication patterns, legacy integrations — check that your requirements are covered before switching.",[15,5378,5380],{"id":5379},"when-to-use-hono","When to Use Hono",[20,5382,5383,5386],{},[26,5384,5385],{},"New projects with TypeScript."," The TypeScript developer experience is meaningfully better. Start new projects on Hono.",[20,5388,5389,5392],{},[26,5390,5391],{},"Edge deployment is a requirement."," This is the decisive factor. If you are deploying to Cloudflare Workers, Vercel Edge, or any V8-based edge runtime, Express is not an option.",[20,5394,5395,5398],{},[26,5396,5397],{},"The RPC client pattern fits your architecture."," If you have a monorepo with a Nuxt/Next.js frontend, Hono's RPC mode provides end-to-end type safety that eliminates an entire category of API integration bugs.",[20,5400,5401,5404],{},[26,5402,5403],{},"Performance is a priority."," If you are building a high-throughput API or a service where cold start time matters, Hono's performance advantage is real.",[20,5406,5407],{},"My current default for new backend services is Hono. The TypeScript support is better, the performance is better, the edge compatibility keeps more deployment options open, and the API is clean. The ecosystem is smaller but sufficient for most use cases.",[20,5409,5410],{},"For teams that are heavily invested in Express, the migration is not urgent. Express works fine. But for new projects, I would reach for Hono without hesitation.",[40,5412],{},[20,5414,5415,5416,1138],{},"Building a new backend service and evaluating your framework options? I am happy to help think through the trade-offs for your specific requirements. Book a call: ",[690,5417,695],{"href":692,"rel":5418},[694],[40,5420],{},[15,5422,702],{"id":701},[304,5424,5425,5431,5437,5443],{},[307,5426,5427],{},[690,5428,5430],{"href":5429},"/blog/background-jobs-nodejs","Background Jobs in Node.js: Queues, Workers, and Failure Recovery",[307,5432,5433],{},[690,5434,5436],{"href":5435},"/blog/nodejs-performance-optimization","Node.js Performance Optimization: The Practical Guide",[307,5438,5439],{},[690,5440,5442],{"href":5441},"/blog/typescript-backend-development","TypeScript for Backend Development: Patterns I Use on Every Project",[307,5444,5445],{},[690,5446,5448],{"href":5447},"/blog/code-review-best-practices","Code Review Best Practices: Making Reviews Worth Everyone's Time",[730,5450,5451],{},"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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":62,"searchDepth":97,"depth":97,"links":5453},[5454,5455,5456,5457,5458,5459,5460,5461,5462],{"id":4723,"depth":80,"text":4724},{"id":4736,"depth":80,"text":4737},{"id":4997,"depth":80,"text":759},{"id":5027,"depth":80,"text":5028},{"id":5304,"depth":80,"text":5305},{"id":5343,"depth":80,"text":5344},{"id":5357,"depth":80,"text":5358},{"id":5379,"depth":80,"text":5380},{"id":701,"depth":80,"text":702},"A practical comparison of Hono and Express for TypeScript backend development — performance, middleware ecosystem, edge compatibility, TypeScript support, and when to choose each.",[5465,5466],"Hono vs Express","Node.js framework",{},"/blog/hono-vs-express",{"title":4714,"description":5463},"blog/hono-vs-express",[5313,5472,5473],"Hono","Express","gXpVQlZtbPWnSmVVJ2w3ymJb3iEYdZY6aCE1e0mv2k4",{"id":5476,"title":5477,"author":5478,"body":5479,"category":4112,"date":746,"description":5744,"extension":748,"featured":749,"image":750,"keywords":5745,"meta":5750,"navigation":458,"path":5751,"readTime":455,"seo":5752,"stem":5753,"tags":5754,"__hash__":5756},"blog/blog/how-to-become-a-software-architect.md","How to Become a Software Architect (A Practitioner's Path)",{"name":9,"bio":10},{"type":12,"value":5480,"toc":5723},[5481,5485,5488,5491,5493,5497,5500,5503,5529,5532,5534,5538,5541,5544,5548,5551,5555,5558,5562,5565,5567,5571,5574,5578,5584,5590,5596,5602,5604,5608,5611,5614,5620,5626,5632,5635,5637,5641,5645,5648,5652,5655,5659,5662,5666,5669,5671,5675,5678,5681,5684,5686,5693,5695,5697],[15,5482,5484],{"id":5483},"theres-no-certification-that-gets-you-here","There's No Certification That Gets You Here",[20,5486,5487],{},"The first thing I want to dispel: there's no exam you can pass, no bootcamp you can complete, no certification program you can finish that makes you a software architect. There are certification programs that will take your money. There are conferences that will hand you a badge. But the actual progression from engineer to architect happens through accumulated judgment built by doing the work and, more importantly, by paying attention while you do it.",[20,5489,5490],{},"That said, the path isn't random. There are specific things you can do — skills to build, projects to pursue, habits to develop — that accelerate the transition. Here's what actually works.",[40,5492],{},[15,5494,5496],{"id":5495},"start-with-a-strong-engineering-foundation","Start With a Strong Engineering Foundation",[20,5498,5499],{},"You cannot architect systems you don't understand at the implementation level. Every architect I've met who struggles with credibility has the same gap: they moved away from code too early and their instincts didn't develop.",[20,5501,5502],{},"Before you start focusing on architecture, make sure you can:",[304,5504,5505,5511,5517,5523],{},[307,5506,5507,5510],{},[26,5508,5509],{},"Build and ship complete systems."," Not tutorials, not side projects that never see users. Actual software that handles real load, real edge cases, and real failure scenarios.",[307,5512,5513,5516],{},[26,5514,5515],{},"Debug production issues under pressure."," Architecture instincts are built by understanding failure modes, and you only understand failure modes by experiencing them.",[307,5518,5519,5522],{},[26,5520,5521],{},"Read and understand code you didn't write."," You'll spend a significant portion of your career as an architect understanding existing systems before proposing changes to them.",[307,5524,5525,5528],{},[26,5526,5527],{},"Understand your data layer."," Database design, query optimization, indexing strategy, and transaction semantics are non-negotiable fundamentals.",[20,5530,5531],{},"The depth matters more than the breadth at this stage. Being very good at one or two technology stacks is more valuable than being surface-level familiar with a dozen.",[40,5533],{},[15,5535,5537],{"id":5536},"develop-system-thinking-deliberately","Develop System Thinking Deliberately",[20,5539,5540],{},"At some point, the transition from engineer to architect requires a shift in what you pay attention to. Engineers naturally focus on the component they're working on. Architects focus on how components interact.",[20,5542,5543],{},"Start practicing this shift while you're still in an engineering role:",[1711,5545,5547],{"id":5546},"ask-why-about-every-design-decision","Ask \"Why\" About Every Design Decision",[20,5549,5550],{},"When your team decides to use a message queue instead of direct API calls, ask why. Not to be disruptive — ask to understand the trade-off. When someone proposes caching a database query, ask what happens when the cache is cold, when it's stale, and how you invalidate it. This isn't pedantry. It's the beginning of architectural thinking.",[1711,5552,5554],{"id":5553},"study-systems-you-didnt-build","Study Systems You Didn't Build",[20,5556,5557],{},"Read engineering blogs from companies operating at scale. Netflix, Uber, Slack, Discord — all of them publish detailed post-mortems and architectural write-ups. Read them not for the technology they chose, but for the problem they were trying to solve and the constraints they were operating under. The technology is almost secondary.",[1711,5559,5561],{"id":5560},"draw-your-current-system","Draw Your Current System",[20,5563,5564],{},"Without looking anything up, draw the system you work on every day. Every service, every database, every queue, every external dependency. Include data flows and failure modes. If you can't draw it accurately from memory, you don't understand it well enough — and neither does anyone else on your team, which is an architectural risk.",[40,5566],{},[15,5568,5570],{"id":5569},"take-on-projects-that-stretch-your-scope","Take On Projects That Stretch Your Scope",[20,5572,5573],{},"The career progression to architect happens when you start taking on work that has broader scope than your current role technically requires. You don't wait for permission to think architecturally — you start doing it.",[1711,5575,5577],{"id":5576},"projects-worth-pursuing","Projects worth pursuing:",[20,5579,5580,5583],{},[26,5581,5582],{},"Cross-team integration work."," Volunteer to own the integration between your team's service and another team's. You'll immediately confront the questions architects live in: interface design, versioning, failure handling, coordination.",[20,5585,5586,5589],{},[26,5587,5588],{},"Migration or refactoring initiatives."," Moving from one technology to another, splitting a monolith into services, upgrading a legacy system — these force you to think about transition states, rollback strategies, and incremental delivery.",[20,5591,5592,5595],{},[26,5593,5594],{},"Developer tooling and DX improvements."," Build the deployment pipeline, the local dev environment, the testing infrastructure. These are unglamorous, but they teach you how the whole system comes together and how teams actually work.",[20,5597,5598,5601],{},[26,5599,5600],{},"Incident analysis."," Every production incident is a masterclass in what your architecture didn't account for. Write the post-mortem. Identify the systemic contributing factors. Propose the structural fix, not just the immediate patch.",[40,5603],{},[15,5605,5607],{"id":5606},"learn-to-communicate-across-levels","Learn to Communicate Across Levels",[20,5609,5610],{},"Architecture is ultimately a communication discipline. A design is only useful if it can be understood, evaluated, and acted on by the people who need to implement and fund it.",[20,5612,5613],{},"You need to become comfortable explaining technical decisions to three different audiences:",[20,5615,5616,5619],{},[26,5617,5618],{},"Engineers:"," They need technical precision. They'll challenge your approach on implementation details, and they should. Architectural decisions need to survive engineering scrutiny.",[20,5621,5622,5625],{},[26,5623,5624],{},"Technical managers:"," They need to understand scope, risk, and resource implications. They're asking: what does this cost, what happens if it fails, how long does it take?",[20,5627,5628,5631],{},[26,5629,5630],{},"Business stakeholders:"," They don't care about the technology. They care whether the system can support the business capability they need, and whether it can do it reliably. Learn to translate technical constraints into business terms without dumbing them down.",[20,5633,5634],{},"If you can only communicate in one direction, your architecture will get blocked or misimplemented.",[40,5636],{},[15,5638,5640],{"id":5639},"common-mistakes-that-slow-the-transition","Common Mistakes That Slow the Transition",[1711,5642,5644],{"id":5643},"over-engineering-your-first-architect-level-designs","Over-engineering your first \"architect-level\" designs",[20,5646,5647],{},"The trap most engineers fall into when they start thinking architecturally is reaching for complexity too quickly. Event sourcing, CQRS, microservices — they're all real patterns with real use cases. They're also overkill for most problems. The hardest part of architecture isn't knowing the patterns. It's knowing when not to apply them.",[1711,5649,5651],{"id":5650},"losing-touch-with-the-code","Losing touch with the code",[20,5653,5654],{},"Architects who stop reading production code lose credibility fast, and they deserve to. You cannot make sound structural decisions about a system you don't understand at the implementation level. Stay in the code, at least enough to review it seriously.",[1711,5656,5658],{"id":5657},"making-decisions-unilaterally","Making decisions unilaterally",[20,5660,5661],{},"Architecture is not a solo activity. The best architects I know are the ones who enable decisions rather than hand down edicts. They create the space for engineering teams to contribute to architectural thinking, because the people closest to the problem often have the best insights.",[1711,5663,5665],{"id":5664},"not-writing-things-down","Not writing things down",[20,5667,5668],{},"Every significant architectural decision should be documented. Not because it proves you were right — but because it captures the context, constraints, and alternatives considered at the time. A year from now, when someone wants to reverse that decision, the ADR explains why it was made and what tradeoffs were accepted. Without it, you're relitigating solved problems.",[40,5670],{},[15,5672,5674],{"id":5673},"the-timeline-is-longer-than-you-think","The Timeline Is Longer Than You Think",[20,5676,5677],{},"Most engineers who become architects spend 8-12 years in engineering roles first. Some do it faster with exceptional exposure. The timeline isn't arbitrary — it's a function of how long it takes to accumulate enough failure experiences and system-level exposure to develop genuine architectural judgment.",[20,5679,5680],{},"There are no shortcuts around the experience. There are ways to get better experience faster: work on systems that have real users, real scale, and real consequences for getting it wrong. Seek out mentors who can accelerate your system-thinking. Write publicly about what you're learning — it forces clarity.",[20,5682,5683],{},"But the most important thing you can do is care about the parts of your current work that don't feel like they're your job. The parts where the system doesn't fit together cleanly. The places where teams are blocked by missing decisions. That's where architecture lives, and that's where your transition begins.",[40,5685],{},[20,5687,5688,5689],{},"If you're working through your own path toward technical leadership and want a direct conversation about how to accelerate it, ",[690,5690,5692],{"href":692,"rel":5691},[694],"schedule time here.",[40,5694],{},[15,5696,702],{"id":701},[304,5698,5699,5705,5711,5717],{},[307,5700,5701],{},[690,5702,5704],{"href":5703},"/blog/software-architect-vs-software-engineer","Software Architect vs Software Engineer: What's Actually Different",[307,5706,5707],{},[690,5708,5710],{"href":5709},"/blog/what-is-a-software-architect","What Is a Software Architect? (And Why Your Business Needs One)",[307,5712,5713],{},[690,5714,5716],{"href":5715},"/blog/software-architect-skills","The Skills That Separate Software Architects from Senior Developers",[307,5718,5719],{},[690,5720,5722],{"href":5721},"/blog/cqrs-event-sourcing-explained","CQRS and Event Sourcing: A Practitioner's Honest Take",{"title":62,"searchDepth":97,"depth":97,"links":5724},[5725,5726,5727,5732,5735,5736,5742,5743],{"id":5483,"depth":80,"text":5484},{"id":5495,"depth":80,"text":5496},{"id":5536,"depth":80,"text":5537,"children":5728},[5729,5730,5731],{"id":5546,"depth":97,"text":5547},{"id":5553,"depth":97,"text":5554},{"id":5560,"depth":97,"text":5561},{"id":5569,"depth":80,"text":5570,"children":5733},[5734],{"id":5576,"depth":97,"text":5577},{"id":5606,"depth":80,"text":5607},{"id":5639,"depth":80,"text":5640,"children":5737},[5738,5739,5740,5741],{"id":5643,"depth":97,"text":5644},{"id":5650,"depth":97,"text":5651},{"id":5657,"depth":97,"text":5658},{"id":5664,"depth":97,"text":5665},{"id":5673,"depth":80,"text":5674},{"id":701,"depth":80,"text":702},"How to become a software architect isn't a mystery — it's a deliberate progression. Here's the honest career path, the skills that matter, and the mistakes that slow most engineers down.",[5746,5747,5748,5749],"how to become a software architect","software architect career path","software architect skills","architecture career progression",{},"/blog/how-to-become-a-software-architect",{"title":5477,"description":5744},"blog/how-to-become-a-software-architect",[4127,1685,745,5755],"Professional Development","gadvFJtXs4IUpA5EBZlzVEVFOJkgHKJwjObzaIjDlLM",{"id":5758,"title":5759,"author":5760,"body":5761,"category":1685,"date":746,"description":5957,"extension":748,"featured":749,"image":750,"keywords":5958,"meta":5961,"navigation":458,"path":5962,"readTime":265,"seo":5963,"stem":5964,"tags":5965,"__hash__":5969},"blog/blog/how-to-become-it-project-manager.md","How to Become an IT Project Manager (From Developer to Project Lead)",{"name":9,"bio":10},{"type":12,"value":5762,"toc":5947},[5763,5767,5770,5773,5776,5778,5782,5785,5791,5797,5803,5805,5809,5812,5818,5824,5830,5832,5836,5842,5848,5854,5860,5862,5866,5869,5872,5875,5877,5881,5884,5887,5890,5893,5895,5899,5902,5905,5908,5910,5917,5919,5921],[15,5764,5766],{"id":5765},"the-transition-nobody-fully-prepares-you-for","The Transition Nobody Fully Prepares You For",[20,5768,5769],{},"I've watched a lot of talented developers burn out trying to become project managers overnight. They're handed a Jira board, told to \"coordinate the team,\" and expected to somehow know how to run a sprint, manage stakeholder expectations, write a project charter, and still understand what the engineers are doing. It's a rough transition when you do it wrong.",[20,5771,5772],{},"The good news: the path from developer to IT project manager is genuinely navigable. The technical foundation you already have gives you a real edge over PMs who came up through a non-technical path. You'll understand why the team estimates are what they are. You'll catch when a vendor is oversimplifying scope. You'll know when \"almost done\" actually means \"we haven't started the hard part.\"",[20,5774,5775],{},"What you'll need to build is the other half of the job — the human systems, the process scaffolding, and the communication discipline that keeps a project moving without you personally writing code.",[40,5777],{},[15,5779,5781],{"id":5780},"what-it-project-managers-actually-do","What IT Project Managers Actually Do",[20,5783,5784],{},"Let me clear up the mythology first. A project manager is not a task-tracking clerk. Updating Jira tickets is a small piece of the job. The real work happens in three areas:",[20,5786,5787,5790],{},[26,5788,5789],{},"Scope management."," Defining exactly what the project will deliver and holding that line when stakeholders inevitably start adding things. Every feature request that slides in mid-sprint is a symptom of insufficient upfront clarity — and it's your job to prevent it or handle it systematically when it happens anyway.",[20,5792,5793,5796],{},[26,5794,5795],{},"Risk management."," Identifying what can go wrong before it does, quantifying the likelihood and impact, and having a plan for each scenario. On software projects, the most common risks are unclear requirements, integration failures with third-party systems, and key-person dependencies. If you know these going in, you're already ahead.",[20,5798,5799,5802],{},[26,5800,5801],{},"Stakeholder communication."," Translating what's happening technically into terms that matter to the people who funded the project. They don't care about the database migration. They care whether the system will be live before the sales conference. Your job is to connect those two realities clearly, honestly, and on a cadence that doesn't require them to chase you down.",[40,5804],{},[15,5806,5808],{"id":5807},"the-skills-that-transfer-from-development","The Skills That Transfer From Development",[20,5810,5811],{},"More than you think.",[20,5813,5814,5817],{},[26,5815,5816],{},"Systems thinking."," Developers naturally understand dependencies — you can't deploy before the database is migrated, you can't test before the feature is built. This is precisely the mental model you need for project scheduling. A Gantt chart is just a dependency graph with time attached to it.",[20,5819,5820,5823],{},[26,5821,5822],{},"Technical credibility."," This is massive and underrated. When you walk into a status meeting and the dev team says \"we need two more weeks,\" you know how to ask the right questions: Is this a complexity issue or a scope creep issue? Did we underestimate the API integration or did the requirements change? Other PMs have to take those answers on faith. You can investigate.",[20,5825,5826,5829],{},[26,5827,5828],{},"Documentation habits."," Good developers write things down — specs, architecture decisions, API contracts. PMs who can write clear, precise project documentation (not just status reports) are genuinely rare. Bring that skill with you.",[40,5831],{},[15,5833,5835],{"id":5834},"what-youll-need-to-learn","What You'll Need to Learn",[20,5837,5838,5841],{},[26,5839,5840],{},"Estimation at the project level."," Developer estimation is about tasks. PM estimation is about projects — which includes all the overhead, dependencies, review cycles, stakeholder feedback rounds, and the inevitable surprises. You'll need to learn techniques like analogous estimation, parametric estimation, and three-point estimation (optimistic/pessimistic/most likely). These are skills, not magic.",[20,5843,5844,5847],{},[26,5845,5846],{},"Budget tracking."," Most developers have never had to track labor costs, vendor invoices, or explain a budget variance to a VP. Get comfortable with spreadsheets at a financial level. Understand how to calculate earned value, how to report budget-at-completion, and how to have an honest conversation when the project is running hot.",[20,5849,5850,5853],{},[26,5851,5852],{},"Stakeholder management."," This is the part that surprises most developers. The technical problems are rarely the hardest part. Managing an executive who wants to add features in week six, a vendor who isn't delivering, and a business user who keeps changing their mind — simultaneously — is where a lot of PMs struggle. Read up on stakeholder analysis. Learn the difference between managing up and managing sideways.",[20,5855,5856,5859],{},[26,5857,5858],{},"Conflict resolution."," When two senior engineers disagree on approach and the project is stalling because of it, you need a process for breaking the impasse. That's not a technical problem — it's a people problem that requires a different skill set.",[40,5861],{},[15,5863,5865],{"id":5864},"the-certification-question","The Certification Question",[20,5867,5868],{},"Certifications are a chapter unto themselves (I've written separately about which ones actually matter), but here's the short version: the PMP (Project Management Professional) from PMI is the gold standard for credibility on enterprise projects. It requires documented experience and a reasonably rigorous exam. Worth the effort if you're targeting larger organizations or government contracts.",[20,5870,5871],{},"The CSM (Certified Scrum Master) is faster to get and more practical for teams running Agile. If you're managing software teams specifically, this is often more immediately relevant than PMP.",[20,5873,5874],{},"Neither replaces actual experience. They're signals, not substitutes.",[40,5876],{},[15,5878,5880],{"id":5879},"how-to-make-the-move-practically","How to Make the Move Practically",[20,5882,5883],{},"Start by shadowing. If you're currently on a development team, ask to sit in on planning meetings, retrospectives, and stakeholder calls. Observe how decisions get made. Notice what creates confusion and what creates clarity.",[20,5885,5886],{},"Take on informal PM responsibilities. Volunteer to own the documentation for a project. Offer to run the sprint planning meeting. Lead the post-mortem after a rough deployment. These micro-experiences add up, and they're evidence you can point to when you apply for a formal PM role.",[20,5888,5889],{},"Build your external signal. Get one certification. Build a portfolio that shows you've managed something end-to-end — even if it was a side project or volunteer work. Document the scope, the timeline, the challenges, and the outcome. That's a project case study.",[20,5891,5892],{},"Target companies where technical PMs are valued. Consulting firms, enterprise software companies, and government contractors all have strong incentives to hire PMs who can credibly talk to developers and business stakeholders in the same meeting. That's your competitive advantage — use it.",[40,5894],{},[15,5896,5898],{"id":5897},"what-the-first-six-months-looks-like","What the First Six Months Looks Like",[20,5900,5901],{},"Expect to feel incompetent in the people-system parts while feeling overqualified for the technical conversations. That's normal. You'll catch up on the processes faster than you think, because you already understand the underlying work.",[20,5903,5904],{},"The hardest adjustment is letting go of the code. You'll want to jump in and fix the bug yourself, refactor the sloppy function you can see in the pull request, rewrite the architecture doc that's confusing. Resist that. Your job now is to create the conditions under which other people can do those things well. That's a fundamentally different kind of work, and it takes a while to feel satisfying in a different way than shipping code does.",[20,5906,5907],{},"It is satisfying, though. Watching a complex project land cleanly — on time, within budget, with a team that isn't exhausted and burned out — is a different kind of win than a clean deployment. But it's a real one.",[40,5909],{},[20,5911,5912,5913,5916],{},"If you're considering making the shift from developer to project lead and want to think through whether the timing is right for your situation, I'm happy to have that conversation. Book a call at ",[690,5914,695],{"href":692,"rel":5915},[694]," and let's map out a path that makes sense for where you are.",[40,5918],{},[15,5920,702],{"id":701},[304,5922,5923,5929,5935,5941],{},[307,5924,5925],{},[690,5926,5928],{"href":5927},"/blog/it-project-manager-certification","IT Project Manager Certifications: Which Ones Actually Matter",[307,5930,5931],{},[690,5932,5934],{"href":5933},"/blog/building-a-developer-portfolio","Building a Developer Portfolio That Converts: Beyond the GitHub Link",[307,5936,5937],{},[690,5938,5940],{"href":5939},"/blog/developer-productivity-tools","Developer Productivity: The Tools and Habits That Actually Move the Needle",[307,5942,5943],{},[690,5944,5946],{"href":5945},"/blog/technical-interview-guide","Technical Interviews: What They're Actually Testing (And How to Prepare)",{"title":62,"searchDepth":97,"depth":97,"links":5948},[5949,5950,5951,5952,5953,5954,5955,5956],{"id":5765,"depth":80,"text":5766},{"id":5780,"depth":80,"text":5781},{"id":5807,"depth":80,"text":5808},{"id":5834,"depth":80,"text":5835},{"id":5864,"depth":80,"text":5865},{"id":5879,"depth":80,"text":5880},{"id":5897,"depth":80,"text":5898},{"id":701,"depth":80,"text":702},"Making the move from developer to IT project manager is a career shift many engineers consider. Here's the honest path, what skills transfer, and what you'll need to learn.",[5959,5960],"how to become an IT project manager","IT project manager",{},"/blog/how-to-become-it-project-manager",{"title":5759,"description":5957},"blog/how-to-become-it-project-manager",[5966,5967,5968],"Project Management","Career Growth","IT Leadership","BqGKDMsrAfz04_ee-GpavA0oF-RMqmDTgPCb8WhCFVM",{"id":5971,"title":716,"author":5972,"body":5973,"category":745,"date":746,"description":6638,"extension":748,"featured":749,"image":750,"keywords":6639,"meta":6642,"navigation":458,"path":715,"readTime":265,"seo":6643,"stem":6644,"tags":6645,"__hash__":6648},"blog/blog/image-optimization-web.md",{"name":9,"bio":10},{"type":12,"value":5974,"toc":6628},[5975,5979,5982,5985,5987,5991,5996,5999,6005,6011,6017,6023,6029,6037,6147,6150,6152,6156,6159,6167,6238,6247,6253,6262,6264,6268,6271,6274,6277,6280,6285,6305,6307,6311,6314,6319,6368,6374,6383,6390,6496,6499,6501,6505,6508,6514,6517,6520,6546,6549,6551,6555,6558,6589,6592,6594,6601,6603,6605,6625],[15,5976,5978],{"id":5977},"images-are-still-the-biggest-performance-problem-on-most-sites","Images Are Still the Biggest Performance Problem on Most Sites",[20,5980,5981],{},"HTTP Archive data consistently shows that images are the largest contributor to page weight across the web — typically accounting for 50-70% of total page bytes on image-heavy pages. Despite years of tooling improvements, the median page still transfers images that are significantly larger than they need to be.",[20,5983,5984],{},"The good news is that image optimization is one of the highest-leverage performance investments you can make, and most of the gains come from a small number of techniques applied correctly. Here's the complete practical guide.",[40,5986],{},[15,5988,5990],{"id":5989},"format-selection","Format Selection",[20,5992,5993],{},[26,5994,5995],{},"WebP vs AVIF vs JPEG vs PNG",[20,5997,5998],{},"The choice of image format is the first decision, and it has a bigger impact than compression level within a given format.",[20,6000,6001,6004],{},[26,6002,6003],{},"AVIF"," is the best format for most images in 2026 — it offers significantly better compression than WebP (typically 50% smaller than JPEG at equivalent quality) with excellent browser support (Chrome, Firefox, Safari all support it). The trade-off is slower encoding, which matters if you're processing images at request time but is irrelevant if you're pre-processing.",[20,6006,6007,6010],{},[26,6008,6009],{},"WebP"," is the safe default when you need wide compatibility with faster encoding. About 25-35% smaller than JPEG at equivalent visual quality. Fully supported in all modern browsers.",[20,6012,6013,6016],{},[26,6014,6015],{},"JPEG"," is appropriate for photographs when legacy browser support matters or when AVIF/WebP is genuinely unavailable. Use progressive JPEG encoding for large images — it gives users something to look at while the image loads rather than a blank space.",[20,6018,6019,6022],{},[26,6020,6021],{},"PNG"," is appropriate for images that require transparency (logos, icons with transparent backgrounds) or images with large areas of solid color (diagrams, screenshots) where PNG's lossless compression outperforms JPEG's lossy compression. Never use PNG for photographs.",[20,6024,6025,6028],{},[26,6026,6027],{},"SVG"," is appropriate for icons, logos, and simple graphics. SVG scales perfectly to any resolution, compresses very well, and can be styled with CSS. A site that uses PNG sprites for icons when SVG is available is leaving significant optimization on the table.",[20,6030,6031],{},[26,6032,6033,6034,152],{},"Serving multiple formats with ",[47,6035,6036],{},"\u003Cpicture>",[57,6038,6040],{"className":201,"code":6039,"language":203,"meta":62,"style":62},"\u003Cpicture>\n \u003Csource srcset=\"hero.avif\" type=\"image/avif\">\n \u003Csource srcset=\"hero.webp\" type=\"image/webp\">\n \u003Cimg src=\"hero.jpg\" alt=\"Hero description\" width=\"1200\" height=\"630\">\n\u003C/picture>\n",[47,6041,6042,6051,6076,6098,6138],{"__ignoreMap":62},[66,6043,6044,6046,6049],{"class":68,"line":69},[66,6045,210],{"class":76},[66,6047,6048],{"class":213},"picture",[66,6050,268],{"class":76},[66,6052,6053,6056,6059,6062,6064,6067,6069,6071,6074],{"class":68,"line":80},[66,6054,6055],{"class":76}," \u003C",[66,6057,6058],{"class":213},"source",[66,6060,6061],{"class":219}," srcset",[66,6063,223],{"class":76},[66,6065,6066],{"class":90},"\"hero.avif\"",[66,6068,251],{"class":219},[66,6070,223],{"class":76},[66,6072,6073],{"class":90},"\"image/avif\"",[66,6075,268],{"class":76},[66,6077,6078,6080,6082,6084,6086,6089,6091,6093,6096],{"class":68,"line":97},[66,6079,6055],{"class":76},[66,6081,6058],{"class":213},[66,6083,6061],{"class":219},[66,6085,223],{"class":76},[66,6087,6088],{"class":90},"\"hero.webp\"",[66,6090,251],{"class":219},[66,6092,223],{"class":76},[66,6094,6095],{"class":90},"\"image/webp\"",[66,6097,268],{"class":76},[66,6099,6100,6102,6105,6107,6109,6112,6115,6117,6120,6123,6125,6128,6131,6133,6136],{"class":68,"line":128},[66,6101,6055],{"class":76},[66,6103,6104],{"class":213},"img",[66,6106,100],{"class":219},[66,6108,223],{"class":76},[66,6110,6111],{"class":90},"\"hero.jpg\"",[66,6113,6114],{"class":219}," alt",[66,6116,223],{"class":76},[66,6118,6119],{"class":90},"\"Hero description\"",[66,6121,6122],{"class":219}," width",[66,6124,223],{"class":76},[66,6126,6127],{"class":90},"\"1200\"",[66,6129,6130],{"class":219}," height",[66,6132,223],{"class":76},[66,6134,6135],{"class":90},"\"630\"",[66,6137,268],{"class":76},[66,6139,6140,6143,6145],{"class":68,"line":141},[66,6141,6142],{"class":76},"\u003C/",[66,6144,6048],{"class":213},[66,6146,268],{"class":76},[20,6148,6149],{},"The browser picks the first format it supports. All browsers that support AVIF use it; browsers that support WebP but not AVIF use WebP; everything else gets JPEG.",[40,6151],{},[15,6153,6155],{"id":6154},"responsive-images","Responsive Images",[20,6157,6158],{},"Serving a 2400px-wide image to a user on a 375px mobile device is sending 6x more data than the user needs. Responsive images solve this.",[20,6160,6161],{},[26,6162,45,6163,6166],{},[47,6164,6165],{},"srcset"," attribute:",[57,6168,6170],{"className":201,"code":6169,"language":203,"meta":62,"style":62},"\u003Cimg\n src=\"product-600w.jpg\"\n srcset=\"product-600w.jpg 600w, product-1200w.jpg 1200w, product-2400w.jpg 2400w\"\n sizes=\"(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n alt=\"Product name\"\n width=\"600\"\n height=\"400\"\n>\n",[47,6171,6172,6179,6188,6197,6207,6216,6225,6234],{"__ignoreMap":62},[66,6173,6174,6176],{"class":68,"line":69},[66,6175,210],{"class":76},[66,6177,6178],{"class":213},"img\n",[66,6180,6181,6183,6185],{"class":68,"line":80},[66,6182,100],{"class":219},[66,6184,223],{"class":76},[66,6186,6187],{"class":90},"\"product-600w.jpg\"\n",[66,6189,6190,6192,6194],{"class":68,"line":97},[66,6191,6061],{"class":219},[66,6193,223],{"class":76},[66,6195,6196],{"class":90},"\"product-600w.jpg 600w, product-1200w.jpg 1200w, product-2400w.jpg 2400w\"\n",[66,6198,6199,6202,6204],{"class":68,"line":128},[66,6200,6201],{"class":219}," sizes",[66,6203,223],{"class":76},[66,6205,6206],{"class":90},"\"(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n",[66,6208,6209,6211,6213],{"class":68,"line":141},[66,6210,6114],{"class":219},[66,6212,223],{"class":76},[66,6214,6215],{"class":90},"\"Product name\"\n",[66,6217,6218,6220,6222],{"class":68,"line":259},[66,6219,6122],{"class":219},[66,6221,223],{"class":76},[66,6223,6224],{"class":90},"\"600\"\n",[66,6226,6227,6229,6231],{"class":68,"line":265},[66,6228,6130],{"class":219},[66,6230,223],{"class":76},[66,6232,6233],{"class":90},"\"400\"\n",[66,6235,6236],{"class":68,"line":450},[66,6237,268],{"class":76},[20,6239,45,6240,6242,6243,6246],{},[47,6241,6165],{}," tells the browser what images are available and their widths. The ",[47,6244,6245],{},"sizes"," tells the browser how wide the image will be displayed at different viewport widths. The browser uses both to pick the most appropriate image source.",[20,6248,6249,6252],{},[26,6250,6251],{},"The rule of thumb:"," Provide image sizes at 1x, 1.5x, and 2x the maximum CSS display size. If your image displays at 600px maximum, provide 600w, 900w, and 1200w versions.",[20,6254,6255,6258,6259,6261],{},[26,6256,6257],{},"For high-DPI screens:"," Devices with 2x or 3x pixel density need larger images to look sharp. The ",[47,6260,6165],{}," approach handles this automatically — the browser requests a larger image on a high-DPI display.",[40,6263],{},[15,6265,6267],{"id":6266},"compression-and-quality-settings","Compression and Quality Settings",[20,6269,6270],{},"The quality setting in lossy formats is the single biggest variable after format choice. Most developers use tool defaults that are often too conservative.",[20,6272,6273],{},"For JPEG: quality 75-85 is indistinguishable from 95-100 at typical web display sizes. Quality 60-75 is usually acceptable for thumbnails and secondary images. Quality 85 as a default for hero images.",[20,6275,6276],{},"For WebP: quality 75-85. The WebP quality scale maps roughly to JPEG quality but produces smaller files.",[20,6278,6279],{},"For AVIF: quality 50-65 typically produces results visually equivalent to JPEG quality 80-90 at significantly smaller file size. Experiment with your specific image content.",[20,6281,6282],{},[26,6283,6284],{},"Tools for batch processing:",[304,6286,6287,6293,6299,6302],{},[307,6288,6289,6292],{},[47,6290,6291],{},"sharp"," (Node.js) — fast, high-quality, excellent for build tooling",[307,6294,6295,6298],{},[47,6296,6297],{},"squoosh"," (CLI) — excellent quality with modern format support",[307,6300,6301],{},"ImageMagick — the swiss army knife, available in most CI environments",[307,6303,6304],{},"Cloudflare Images / AWS CloudFront image optimization — CDN-level optimization without build step",[40,6306],{},[15,6308,6310],{"id":6309},"lazy-loading","Lazy Loading",[20,6312,6313],{},"Images below the fold don't need to load until the user scrolls down. Lazy loading defers their download, reducing initial page weight and improving LCP (which is measured from the top of the page).",[20,6315,6316],{},[26,6317,6318],{},"Native lazy loading:",[57,6320,6322],{"className":201,"code":6321,"language":203,"meta":62,"style":62},"\u003Cimg src=\"product.jpg\" alt=\"...\" loading=\"lazy\" width=\"800\" height=\"600\">\n",[47,6323,6324],{"__ignoreMap":62},[66,6325,6326,6328,6330,6332,6334,6337,6339,6341,6344,6347,6349,6352,6354,6356,6359,6361,6363,6366],{"class":68,"line":69},[66,6327,210],{"class":76},[66,6329,6104],{"class":213},[66,6331,100],{"class":219},[66,6333,223],{"class":76},[66,6335,6336],{"class":90},"\"product.jpg\"",[66,6338,6114],{"class":219},[66,6340,223],{"class":76},[66,6342,6343],{"class":90},"\"...\"",[66,6345,6346],{"class":219}," loading",[66,6348,223],{"class":76},[66,6350,6351],{"class":90},"\"lazy\"",[66,6353,6122],{"class":219},[66,6355,223],{"class":76},[66,6357,6358],{"class":90},"\"800\"",[66,6360,6130],{"class":219},[66,6362,223],{"class":76},[66,6364,6365],{"class":90},"\"600\"",[66,6367,268],{"class":76},[20,6369,45,6370,6373],{},[47,6371,6372],{},"loading=\"lazy\""," attribute is supported in all modern browsers and requires zero JavaScript. The browser defers loading images until they're within a certain distance of the viewport (the exact threshold varies by browser but is typically about 1200px on desktop).",[20,6375,6376,6379,6380,6382],{},[26,6377,6378],{},"Important:"," Do not use ",[47,6381,6372],{}," on the LCP image. The LCP image is above the fold and should load as fast as possible. Lazy loading it delays LCP.",[20,6384,6385],{},[26,6386,45,6387,6166],{},[47,6388,6389],{},"fetchpriority",[57,6391,6393],{"className":201,"code":6392,"language":203,"meta":62,"style":62},"\u003C!-- Highest priority for LCP image -->\n\u003Cimg src=\"hero.jpg\" fetchpriority=\"high\" alt=\"...\" width=\"1200\" height=\"630\">\n\n\u003C!-- Lower priority for below-fold images -->\n\u003Cimg src=\"product.jpg\" fetchpriority=\"low\" loading=\"lazy\" alt=\"...\" width=\"400\" height=\"300\">\n",[47,6394,6395,6400,6440,6444,6449],{"__ignoreMap":62},[66,6396,6397],{"class":68,"line":69},[66,6398,6399],{"class":582},"\u003C!-- Highest priority for LCP image -->\n",[66,6401,6402,6404,6406,6408,6410,6412,6415,6417,6420,6422,6424,6426,6428,6430,6432,6434,6436,6438],{"class":68,"line":80},[66,6403,210],{"class":76},[66,6405,6104],{"class":213},[66,6407,100],{"class":219},[66,6409,223],{"class":76},[66,6411,6111],{"class":90},[66,6413,6414],{"class":219}," fetchpriority",[66,6416,223],{"class":76},[66,6418,6419],{"class":90},"\"high\"",[66,6421,6114],{"class":219},[66,6423,223],{"class":76},[66,6425,6343],{"class":90},[66,6427,6122],{"class":219},[66,6429,223],{"class":76},[66,6431,6127],{"class":90},[66,6433,6130],{"class":219},[66,6435,223],{"class":76},[66,6437,6135],{"class":90},[66,6439,268],{"class":76},[66,6441,6442],{"class":68,"line":97},[66,6443,459],{"emptyLinePlaceholder":458},[66,6445,6446],{"class":68,"line":128},[66,6447,6448],{"class":582},"\u003C!-- Lower priority for below-fold images -->\n",[66,6450,6451,6453,6455,6457,6459,6461,6463,6465,6468,6470,6472,6474,6476,6478,6480,6482,6484,6487,6489,6491,6494],{"class":68,"line":141},[66,6452,210],{"class":76},[66,6454,6104],{"class":213},[66,6456,100],{"class":219},[66,6458,223],{"class":76},[66,6460,6336],{"class":90},[66,6462,6414],{"class":219},[66,6464,223],{"class":76},[66,6466,6467],{"class":90},"\"low\"",[66,6469,6346],{"class":219},[66,6471,223],{"class":76},[66,6473,6351],{"class":90},[66,6475,6114],{"class":219},[66,6477,223],{"class":76},[66,6479,6343],{"class":90},[66,6481,6122],{"class":219},[66,6483,223],{"class":76},[66,6485,6486],{"class":90},"\"400\"",[66,6488,6130],{"class":219},[66,6490,223],{"class":76},[66,6492,6493],{"class":90},"\"300\"",[66,6495,268],{"class":76},[20,6497,6498],{},"This gives the browser explicit priority hints to load what matters first.",[40,6500],{},[15,6502,6504],{"id":6503},"image-cdn-and-transformation-services","Image CDN and Transformation Services",[20,6506,6507],{},"An image CDN is a service that stores your original images and serves optimized, resized, reformatted versions on demand at the CDN edge. The request URL encodes the transformation parameters:",[57,6509,6512],{"className":6510,"code":6511,"language":2905},[2903],"https://imagecdn.com/img/w=800,h=600,format=webp,quality=80/product-original.jpg\n",[47,6513,6511],{"__ignoreMap":62},[20,6515,6516],{},"This eliminates the need to pre-generate every size/format combination. The CDN generates and caches each variant on first request.",[20,6518,6519],{},"Major options:",[304,6521,6522,6528,6534,6540],{},[307,6523,6524,6527],{},[26,6525,6526],{},"Cloudflare Images"," — good integration if you're already on Cloudflare",[307,6529,6530,6533],{},[26,6531,6532],{},"Imgix"," — feature-rich, excellent documentation",[307,6535,6536,6539],{},[26,6537,6538],{},"Cloudinary"," — adds AI features like automatic cropping and background removal",[307,6541,6542,6545],{},[26,6543,6544],{},"Next.js Image component / Nuxt Image component"," — framework-native, built-in image optimization",[20,6547,6548],{},"For most projects, a framework-native image component or a CDN-level optimization service is simpler than managing the image processing pipeline yourself.",[40,6550],{},[15,6552,6554],{"id":6553},"a-quick-audit-process","A Quick Audit Process",[20,6556,6557],{},"Run this audit on any site you're optimizing:",[1239,6559,6560,6563,6566,6569,6577,6584],{},[307,6561,6562],{},"Open Chrome DevTools Network tab, filter by \"Img\"",[307,6564,6565],{},"Look for images over 100KB serving as JPEG or PNG where WebP/AVIF would work",[307,6567,6568],{},"Look for images significantly larger than their display size (natural width >> display width)",[307,6570,6571,6572,1376,6574,6576],{},"Look for images without ",[47,6573,1375],{},[47,6575,1379],{}," attributes (CLS risk)",[307,6578,6579,6580,6583],{},"Look for images above the fold without ",[47,6581,6582],{},"fetchpriority=\"high\""," on the LCP candidate",[307,6585,6586,6587],{},"Look for images below the fold without ",[47,6588,6372],{},[20,6590,6591],{},"Run Google PageSpeed Insights and check the \"Opportunities\" section — it identifies all the specific images causing problems with concrete size savings estimates.",[40,6593],{},[20,6595,6596,6597,6600],{},"Image optimization is one of the few areas where a few hours of work produces measurable, permanent improvements to both user experience and search rankings. If you're working on a site with large, unoptimized images, book a call at ",[690,6598,695],{"href":692,"rel":6599},[694]," and let's build the optimization plan.",[40,6602],{},[15,6604,702],{"id":701},[304,6606,6607,6613,6617,6621],{},[307,6608,6609],{},[690,6610,6612],{"href":6611},"/blog/nuxt-image-optimization","Image Optimization in Nuxt: @nuxt/image and Beyond",[307,6614,6615],{},[690,6616,710],{"href":709},[307,6618,6619],{},[690,6620,7],{"href":755},[307,6622,6623],{},[690,6624,728],{"href":727},[730,6626,6627],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":62,"searchDepth":97,"depth":97,"links":6629},[6630,6631,6632,6633,6634,6635,6636,6637],{"id":5977,"depth":80,"text":5978},{"id":5989,"depth":80,"text":5990},{"id":6154,"depth":80,"text":6155},{"id":6266,"depth":80,"text":6267},{"id":6309,"depth":80,"text":6310},{"id":6503,"depth":80,"text":6504},{"id":6553,"depth":80,"text":6554},{"id":701,"depth":80,"text":702},"Images are the largest contributor to page weight on most sites. Here's a complete guide to format selection, compression, responsive images, and lazy loading done right.",[6640,6641],"image optimization web","WebP optimization",{},{"title":716,"description":6638},"blog/image-optimization-web",[759,6646,6647],"Images","Web Optimization","j9O_vQCAR_E42RXjqG_0zS8LM_abKbX6ji_hZjGe3ZY",{"id":6650,"title":6651,"author":6652,"body":6653,"category":2835,"date":746,"description":6950,"extension":748,"featured":749,"image":750,"keywords":6951,"meta":6954,"navigation":458,"path":6955,"readTime":265,"seo":6956,"stem":6957,"tags":6958,"__hash__":6962},"blog/blog/incident-response-small-teams.md","Incident Response for Small Teams: Runbooks, Alerts, and Post-Mortems",{"name":9,"bio":10},{"type":12,"value":6654,"toc":6941},[6655,6658,6661,6664,6668,6671,6674,6680,6686,6692,6695,6699,6702,6705,6708,6711,6715,6718,6721,6723,6728,6731,6734,6758,6761,6796,6799,6802,6804,6807,6811,6814,6817,6820,6823,6826,6836,6839,6855,6859,6862,6865,6868,6874,6880,6886,6892,6896,6899,6902,6905,6907,6913,6915,6917],[1694,6656,6651],{"id":6657},"incident-response-for-small-teams-runbooks-alerts-and-post-mortems",[20,6659,6660],{},"The first production incident most small teams experience goes roughly the same way. Something breaks. Someone notices from a user complaint or a red alert. Everyone piles into a Slack channel. Three people are making changes simultaneously with no coordination. One person is trying to diagnose while another person is already reverting a deployment. Communication with affected users is inconsistent or nonexistent. The incident resolves through a combination of correct action and luck, and nobody is sure which. Forty-five minutes of chaos produces a five-minute fix.",[20,6662,6663],{},"This does not have to be how it goes. Even a three-person team can have a functional incident response process. Here is how I think about it.",[15,6665,6667],{"id":6666},"define-what-an-incident-is","Define What an Incident Is",[20,6669,6670],{},"Before you can respond to incidents consistently, you need a shared definition of what counts as an incident. Not every alert is an incident. Not every user complaint requires incident declaration.",[20,6672,6673],{},"A simple severity taxonomy:",[20,6675,6676,6679],{},[26,6677,6678],{},"SEV-1 (Critical)"," — complete service outage, data loss or corruption, security breach, payment processing failure. All hands. Immediate response. Customer communication within 15 minutes.",[20,6681,6682,6685],{},[26,6683,6684],{},"SEV-2 (Major)"," — significant functionality degraded, key user flows broken but workarounds exist, performance degraded enough to affect user experience measurably. On-call engineer responds within 30 minutes.",[20,6687,6688,6691],{},[26,6689,6690],{},"SEV-3 (Minor)"," — minor feature broken, cosmetic issues, single-user issues. Normal ticket queue, addressed in next sprint if not blocking.",[20,6693,6694],{},"Having this defined in advance removes the debate during an active incident about whether something is \"serious enough.\" Someone declares a SEV-1 or SEV-2 and the response process kicks in automatically.",[15,6696,6698],{"id":6697},"the-on-call-rotation","The On-Call Rotation",[20,6700,6701],{},"For a team of any size, someone needs to be designated as the person who responds to alerts outside business hours. That responsibility needs to rotate so it does not fall permanently on one person.",[20,6703,6704],{},"PagerDuty, OpsGenie, and Grafana OnCall all handle on-call scheduling and alert routing. OpsGenie's free tier covers small teams. Grafana OnCall is open-source and integrates with Grafana's monitoring stack.",[20,6706,6707],{},"Configure your alerting tool to page the on-call person for SEV-1 and SEV-2 alerts. A SEV-1 should page immediately and escalate to a second person after 5 minutes of no acknowledgment. The escalation ensures alerts do not go unacknowledged because the on-call person is unavailable.",[20,6709,6710],{},"Set expectations explicitly: on-call means you respond to SEV-1 alerts within 15 minutes, 24/7, during your rotation. Off-call means you are not responsible for after-hours alerts. Rotate weekly, and compensate the on-call burden explicitly (comp time, pay, whatever fits your team). Unacknowledged on-call burden causes burnout.",[15,6712,6714],{"id":6713},"runbooks-the-playbooks-for-common-incidents","Runbooks: The Playbooks for Common Incidents",[20,6716,6717],{},"A runbook is a documented procedure for a specific operational scenario. When an on-call engineer gets paged at 2am, a runbook means they can follow a tested procedure rather than improvising from first principles while half-asleep.",[20,6719,6720],{},"Runbooks do not need to be elaborate. A runbook for \"API high error rate\" might be:",[40,6722],{},[20,6724,6725],{},[26,6726,6727],{},"Runbook: API High Error Rate (SEV-2)",[20,6729,6730],{},"Trigger: Error rate > 2% for 5+ minutes",[20,6732,6733],{},"Diagnosis:",[1239,6735,6736,6742,6749,6755],{},[307,6737,6738,6739],{},"Check recent deployments: ",[47,6740,6741],{},"https://github.com/myorg/api/deployments",[307,6743,6744,6745,6748],{},"Check error distribution in logs: Axiom query ",[47,6746,6747],{},"level:error | count() by statusCode"," (last 15 minutes)",[307,6750,6751,6752],{},"Check database connection pool: ",[47,6753,6754],{},"SELECT count(*) FROM pg_stat_activity WHERE state = 'active'",[307,6756,6757],{},"Check external API status pages: Stripe (status.stripe.com), SendGrid (status.sendgrid.com)",[20,6759,6760],{},"Common causes and resolution:",[304,6762,6763,6772,6781,6787],{},[307,6764,6765,6768,6769],{},[26,6766,6767],{},"Recent deployment broke something"," → roll back: ",[47,6770,6771],{},"kubectl rollout undo deployment/api -n production",[307,6773,6774,6777,6778],{},[26,6775,6776],{},"Database connection pool exhausted"," → restart API pods: ",[47,6779,6780],{},"kubectl rollout restart deployment/api -n production",[307,6782,6783,6786],{},[26,6784,6785],{},"Third-party API down"," → check if error is isolated to those endpoints, communicate to users if so",[307,6788,6789,6792,6793],{},[26,6790,6791],{},"Increased traffic causing overload"," → scale up replicas: ",[47,6794,6795],{},"kubectl scale deployment api --replicas=5 -n production",[20,6797,6798],{},"Escalation: If not resolved in 30 minutes, escalate to Engineering Lead.",[20,6800,6801],{},"Communication: Post status update in #status Slack channel every 15 minutes.",[40,6803],{},[20,6805,6806],{},"This runbook is minimal but complete. An engineer who has never seen this specific failure before can follow it and resolve the most common causes. Build runbooks for your most common alert scenarios. Update them when incidents reveal gaps.",[15,6808,6810],{"id":6809},"communication-during-an-incident","Communication During an Incident",[20,6812,6813],{},"Poor communication during incidents erodes user trust faster than the outage itself. Users who know what is happening and when to expect resolution are more forgiving than users who get no information.",[20,6815,6816],{},"Designate a communicator role in SEV-1 incidents — one person whose job during the incident is customer communication, not technical diagnosis. They post to your status page, respond to support tickets, and update the #status channel. Technical engineers focus on resolution without context-switching to communication.",[20,6818,6819],{},"Your status page should be on a separate hosting provider from your application. If your application is down, your status page needs to still be up. Statuspage.io, BetterUptime, and Instatus all provide externally hosted status pages with automatic incident posting.",[20,6821,6822],{},"Communication cadence: acknowledge within 15 minutes of declaration, update every 30 minutes until resolved, and post a resolution update when the incident ends.",[20,6824,6825],{},"Template for acknowledgment:",[6827,6828,6829],"blockquote",{},[20,6830,6831,6832,6835],{},"We are aware of an issue affecting ",[66,6833,6834],{},"specific feature/service",". Our engineering team is investigating. We will provide an update within 30 minutes.",[20,6837,6838],{},"Template for resolution:",[6827,6840,6841],{},[20,6842,6843,6844,6846,6847,6850,6851,6854],{},"The issue affecting ",[66,6845,6834],{}," has been resolved as of ",[66,6848,6849],{},"time",". Affected users experienced ",[66,6852,6853],{},"brief description",". We will follow up with a full post-mortem within 48 hours.",[15,6856,6858],{"id":6857},"the-post-mortem","The Post-Mortem",[20,6860,6861],{},"The post-mortem is the most important part of incident response, and the most skipped. If you fix the immediate problem and never understand why it happened, you will fix the same problem again.",[20,6863,6864],{},"Post-mortems are blameless. The goal is to understand systemic failures, not to assign fault to individuals. An engineer who made a mistake that contributed to an incident is someone who was working under conditions that allowed that mistake to reach production. The system failed, not the person.",[20,6866,6867],{},"Post-mortem structure:",[20,6869,6870,6873],{},[26,6871,6872],{},"Timeline"," — a chronological record of what happened, when, and who took what action. Build this during or immediately after the incident while memory is fresh.",[20,6875,6876,6879],{},[26,6877,6878],{},"Root cause"," — what actually caused the incident? Keep asking \"why\" until you reach a root cause. \"The API was returning errors\" is a symptom. \"The database connection pool was exhausted\" is closer. \"We deployed without updating connection pool limits to match the new traffic pattern\" is a root cause.",[20,6881,6882,6885],{},[26,6883,6884],{},"Contributing factors"," — what conditions made this incident worse or harder to detect? Missing monitoring, unclear runbooks, confusing deployment process.",[20,6887,6888,6891],{},[26,6889,6890],{},"Action items"," — specific, assignable tasks that reduce the likelihood or impact of this incident recurring. Each action item has an owner and a deadline. Without this, the post-mortem is documentation, not prevention.",[15,6893,6895],{"id":6894},"building-the-process-incrementally","Building the Process Incrementally",[20,6897,6898],{},"You do not need all of this on day one. Build the process incrementally as you encounter incidents.",[20,6900,6901],{},"First incident: write down what happened. That is your first post-mortem. Second incident: designate who is on-call this week. Third incident: write your first runbook for the most common alert. By the time you have had five incidents, you have the skeleton of a real incident response process.",[20,6903,6904],{},"The teams with the best incident response processes got there by taking each incident as an opportunity to improve the process, not as a crisis to survive and forget.",[40,6906],{},[20,6908,6909,6910,1138],{},"Need help building an incident response process for your team or want a second opinion on your current runbooks and alerting setup? Book a session at ",[690,6911,692],{"href":692,"rel":6912},[694],[40,6914],{},[15,6916,702],{"id":701},[304,6918,6919,6923,6929,6935],{},[307,6920,6921],{},[690,6922,3329],{"href":3328},[307,6924,6925],{},[690,6926,6928],{"href":6927},"/blog/cdn-configuration-guide","CDN Configuration: Making Your Static Assets Load Instantly Everywhere",[307,6930,6931],{},[690,6932,6934],{"href":6933},"/blog/cloud-cost-optimization","Cloud Cost Optimization: Cutting the Bill Without Cutting Corners",[307,6936,6937],{},[690,6938,6940],{"href":6939},"/blog/container-security-guide","Container Security: Hardening Docker for Production",{"title":62,"searchDepth":97,"depth":97,"links":6942},[6943,6944,6945,6946,6947,6948,6949],{"id":6666,"depth":80,"text":6667},{"id":6697,"depth":80,"text":6698},{"id":6713,"depth":80,"text":6714},{"id":6809,"depth":80,"text":6810},{"id":6857,"depth":80,"text":6858},{"id":6894,"depth":80,"text":6895},{"id":701,"depth":80,"text":702},"Build an incident response process that works for small engineering teams — on-call rotations, runbooks, communication templates, and post-mortems that prevent recurrence.",[6952,6953],"incident response","on-call development",{},"/blog/incident-response-small-teams",{"title":6651,"description":6950},"blog/incident-response-small-teams",[6959,2835,6960,6961],"Incident Response","On-Call","Operations","zl8-1m1ttctBz68N98KyUdN5jDd5-g7ltyx8arkQEek",{"id":6964,"title":6965,"author":6966,"body":6967,"category":2835,"date":746,"description":7678,"extension":748,"featured":749,"image":750,"keywords":7679,"meta":7682,"navigation":458,"path":7683,"readTime":265,"seo":7684,"stem":7685,"tags":7686,"__hash__":7689},"blog/blog/infrastructure-as-code-guide.md","Infrastructure as Code: Why Your Config Should Live in Git",{"name":9,"bio":10},{"type":12,"value":6968,"toc":7668},[6969,6972,6975,6978,6982,6988,6991,6995,6998,7001,7005,7008,7354,7369,7373,7376,7382,7385,7422,7429,7433,7436,7442,7449,7481,7484,7488,7494,7500,7620,7624,7627,7630,7633,7635,7641,7643,7645,7665],[1694,6970,6965],{"id":6971},"infrastructure-as-code-why-your-config-should-live-in-git",[20,6973,6974],{},"I have inherited enough manually managed infrastructure to have opinions about this. The tell-tale signs are always the same: a server nobody understands fully, a firewall rule added \"temporarily\" two years ago that everyone is afraid to touch, a database configuration that differs between staging and production in ways nobody can explain because it was set up by a developer who left eighteen months ago.",[20,6976,6977],{},"Manual infrastructure is technical debt that compounds. It accumulates undocumented decisions, implicit dependencies, and configuration drift until the day something breaks and nobody knows how to restore it. Infrastructure as Code (IaC) is the solution, and Terraform is the tool I reach for most often.",[15,6979,6981],{"id":6980},"what-infrastructure-as-code-actually-means","What Infrastructure as Code Actually Means",[20,6983,6984,6985,1138],{},"The premise is simple: your infrastructure configuration — servers, databases, DNS records, load balancers, security groups, storage buckets — is defined in files that live in a Git repository. You apply changes by running a command, not by clicking through a web console. History is preserved in commits. Changes go through code review. Rollback is a ",[47,6986,6987],{},"git revert",[20,6989,6990],{},"This is not about following a trendy practice. It is about making your infrastructure reproducible. When your production server dies at 2am, reproducible infrastructure means you can recreate it in thirty minutes. Without IaC, recreating an environment means archaeology — reading through AWS console history, trying to remember what you configured six months ago, hoping the staging environment is close enough to serve as reference.",[15,6992,6994],{"id":6993},"why-terraform","Why Terraform",[20,6996,6997],{},"Terraform is the dominant IaC tool for a reason. It supports virtually every cloud provider through a provider ecosystem, its HCL (HashiCorp Configuration Language) is readable, and the plan/apply workflow makes changes explicit before you execute them. You can see exactly what will change before it changes.",[20,6999,7000],{},"Alternatives exist. Pulumi lets you write infrastructure in TypeScript or Python if you prefer. AWS CloudFormation handles AWS-specific infrastructure natively. For Kubernetes specifically, Helm and Kustomize are better tools. But for general-purpose multi-cloud infrastructure, Terraform is the baseline.",[15,7002,7004],{"id":7003},"a-real-terraform-module","A Real Terraform Module",[20,7006,7007],{},"Here is a simple Terraform configuration for a VPS setup on DigitalOcean — the kind of thing a small production deployment actually needs:",[57,7009,7013],{"className":7010,"code":7011,"language":7012,"meta":62,"style":62},"language-hcl shiki shiki-themes github-dark","terraform {\n required_providers {\n digitalocean = {\n source = \"digitalocean/digitalocean\"\n version = \"~> 2.0\"\n }\n }\n\n backend \"s3\" {\n endpoint = \"https://nyc3.digitaloceanspaces.com\"\n bucket = \"my-terraform-state\"\n key = \"production/terraform.tfstate\"\n region = \"us-east-1\"\n skip_credentials_validation = true\n skip_metadata_api_check = true\n }\n}\n\nVariable \"do_token\" {\n description = \"DigitalOcean API token\"\n type = string\n sensitive = true\n}\n\nVariable \"ssh_key_fingerprint\" {\n description = \"SSH key fingerprint for server access\"\n type = string\n}\n\nProvider \"digitalocean\" {\n token = var.do_token\n}\n\nResource \"digitalocean_droplet\" \"api_server\" {\n name = \"api-production\"\n size = \"s-2vcpu-4gb\"\n image = \"ubuntu-22-04-x64\"\n region = \"nyc3\"\n ssh_keys = [var.ssh_key_fingerprint]\n\n tags = [\"production\", \"api\"]\n}\n\nResource \"digitalocean_firewall\" \"api\" {\n name = \"api-production-firewall\"\n droplet_ids = [digitalocean_droplet.api_server.id]\n\n inbound_rule {\n protocol = \"tcp\"\n port_range = \"22\"\n source_addresses = [\"your.office.ip/32\"]\n }\n\n inbound_rule {\n protocol = \"tcp\"\n port_range = \"443\"\n source_addresses = [\"0.0.0.0/0\", \"::/0\"]\n }\n\n outbound_rule {\n protocol = \"tcp\"\n port_range = \"1-65535\"\n destination_addresses = [\"0.0.0.0/0\", \"::/0\"]\n }\n}\n\nOutput \"server_ip\" {\n value = digitalocean_droplet.api_server.ipv4_address\n}\n","hcl",[47,7014,7015,7020,7025,7030,7035,7040,7044,7048,7052,7057,7062,7067,7072,7077,7082,7087,7091,7095,7099,7104,7109,7114,7119,7123,7127,7132,7137,7141,7145,7149,7154,7159,7163,7167,7172,7177,7182,7187,7192,7197,7201,7206,7210,7214,7219,7224,7229,7233,7239,7245,7251,7257,7262,7267,7272,7277,7283,7289,7294,7299,7305,7310,7316,7322,7327,7332,7337,7343,7349],{"__ignoreMap":62},[66,7016,7017],{"class":68,"line":69},[66,7018,7019],{},"terraform {\n",[66,7021,7022],{"class":68,"line":80},[66,7023,7024],{}," required_providers {\n",[66,7026,7027],{"class":68,"line":97},[66,7028,7029],{}," digitalocean = {\n",[66,7031,7032],{"class":68,"line":128},[66,7033,7034],{}," source = \"digitalocean/digitalocean\"\n",[66,7036,7037],{"class":68,"line":141},[66,7038,7039],{}," version = \"~> 2.0\"\n",[66,7041,7042],{"class":68,"line":259},[66,7043,4976],{},[66,7045,7046],{"class":68,"line":265},[66,7047,4976],{},[66,7049,7050],{"class":68,"line":450},[66,7051,459],{"emptyLinePlaceholder":458},[66,7053,7054],{"class":68,"line":455},[66,7055,7056],{}," backend \"s3\" {\n",[66,7058,7059],{"class":68,"line":462},[66,7060,7061],{}," endpoint = \"https://nyc3.digitaloceanspaces.com\"\n",[66,7063,7064],{"class":68,"line":470},[66,7065,7066],{}," bucket = \"my-terraform-state\"\n",[66,7068,7069],{"class":68,"line":491},[66,7070,7071],{}," key = \"production/terraform.tfstate\"\n",[66,7073,7074],{"class":68,"line":1175},[66,7075,7076],{}," region = \"us-east-1\"\n",[66,7078,7079],{"class":68,"line":1183},[66,7080,7081],{}," skip_credentials_validation = true\n",[66,7083,7084],{"class":68,"line":1191},[66,7085,7086],{}," skip_metadata_api_check = true\n",[66,7088,7089],{"class":68,"line":1846},[66,7090,4976],{},[66,7092,7093],{"class":68,"line":1854},[66,7094,144],{},[66,7096,7097],{"class":68,"line":1865},[66,7098,459],{"emptyLinePlaceholder":458},[66,7100,7101],{"class":68,"line":1876},[66,7102,7103],{},"Variable \"do_token\" {\n",[66,7105,7106],{"class":68,"line":1887},[66,7107,7108],{}," description = \"DigitalOcean API token\"\n",[66,7110,7111],{"class":68,"line":1893},[66,7112,7113],{}," type = string\n",[66,7115,7116],{"class":68,"line":1899},[66,7117,7118],{}," sensitive = true\n",[66,7120,7121],{"class":68,"line":1905},[66,7122,144],{},[66,7124,7125],{"class":68,"line":1911},[66,7126,459],{"emptyLinePlaceholder":458},[66,7128,7129],{"class":68,"line":1917},[66,7130,7131],{},"Variable \"ssh_key_fingerprint\" {\n",[66,7133,7134],{"class":68,"line":1923},[66,7135,7136],{}," description = \"SSH key fingerprint for server access\"\n",[66,7138,7139],{"class":68,"line":1928},[66,7140,7113],{},[66,7142,7143],{"class":68,"line":1934},[66,7144,144],{},[66,7146,7147],{"class":68,"line":1940},[66,7148,459],{"emptyLinePlaceholder":458},[66,7150,7151],{"class":68,"line":1945},[66,7152,7153],{},"Provider \"digitalocean\" {\n",[66,7155,7156],{"class":68,"line":1951},[66,7157,7158],{}," token = var.do_token\n",[66,7160,7161],{"class":68,"line":1957},[66,7162,144],{},[66,7164,7165],{"class":68,"line":1963},[66,7166,459],{"emptyLinePlaceholder":458},[66,7168,7169],{"class":68,"line":1969},[66,7170,7171],{},"Resource \"digitalocean_droplet\" \"api_server\" {\n",[66,7173,7174],{"class":68,"line":1974},[66,7175,7176],{}," name = \"api-production\"\n",[66,7178,7179],{"class":68,"line":1980},[66,7180,7181],{}," size = \"s-2vcpu-4gb\"\n",[66,7183,7184],{"class":68,"line":1985},[66,7185,7186],{}," image = \"ubuntu-22-04-x64\"\n",[66,7188,7189],{"class":68,"line":1991},[66,7190,7191],{}," region = \"nyc3\"\n",[66,7193,7194],{"class":68,"line":1997},[66,7195,7196],{}," ssh_keys = [var.ssh_key_fingerprint]\n",[66,7198,7199],{"class":68,"line":2002},[66,7200,459],{"emptyLinePlaceholder":458},[66,7202,7203],{"class":68,"line":2008},[66,7204,7205],{}," tags = [\"production\", \"api\"]\n",[66,7207,7208],{"class":68,"line":2014},[66,7209,144],{},[66,7211,7212],{"class":68,"line":2019},[66,7213,459],{"emptyLinePlaceholder":458},[66,7215,7216],{"class":68,"line":2025},[66,7217,7218],{},"Resource \"digitalocean_firewall\" \"api\" {\n",[66,7220,7221],{"class":68,"line":2031},[66,7222,7223],{}," name = \"api-production-firewall\"\n",[66,7225,7226],{"class":68,"line":2037},[66,7227,7228],{}," droplet_ids = [digitalocean_droplet.api_server.id]\n",[66,7230,7231],{"class":68,"line":2043},[66,7232,459],{"emptyLinePlaceholder":458},[66,7234,7236],{"class":68,"line":7235},48,[66,7237,7238],{}," inbound_rule {\n",[66,7240,7242],{"class":68,"line":7241},49,[66,7243,7244],{}," protocol = \"tcp\"\n",[66,7246,7248],{"class":68,"line":7247},50,[66,7249,7250],{}," port_range = \"22\"\n",[66,7252,7254],{"class":68,"line":7253},51,[66,7255,7256],{}," source_addresses = [\"your.office.ip/32\"]\n",[66,7258,7260],{"class":68,"line":7259},52,[66,7261,4976],{},[66,7263,7265],{"class":68,"line":7264},53,[66,7266,459],{"emptyLinePlaceholder":458},[66,7268,7270],{"class":68,"line":7269},54,[66,7271,7238],{},[66,7273,7275],{"class":68,"line":7274},55,[66,7276,7244],{},[66,7278,7280],{"class":68,"line":7279},56,[66,7281,7282],{}," port_range = \"443\"\n",[66,7284,7286],{"class":68,"line":7285},57,[66,7287,7288],{}," source_addresses = [\"0.0.0.0/0\", \"::/0\"]\n",[66,7290,7292],{"class":68,"line":7291},58,[66,7293,4976],{},[66,7295,7297],{"class":68,"line":7296},59,[66,7298,459],{"emptyLinePlaceholder":458},[66,7300,7302],{"class":68,"line":7301},60,[66,7303,7304],{}," outbound_rule {\n",[66,7306,7308],{"class":68,"line":7307},61,[66,7309,7244],{},[66,7311,7313],{"class":68,"line":7312},62,[66,7314,7315],{}," port_range = \"1-65535\"\n",[66,7317,7319],{"class":68,"line":7318},63,[66,7320,7321],{}," destination_addresses = [\"0.0.0.0/0\", \"::/0\"]\n",[66,7323,7325],{"class":68,"line":7324},64,[66,7326,4976],{},[66,7328,7330],{"class":68,"line":7329},65,[66,7331,144],{},[66,7333,7335],{"class":68,"line":7334},66,[66,7336,459],{"emptyLinePlaceholder":458},[66,7338,7340],{"class":68,"line":7339},67,[66,7341,7342],{},"Output \"server_ip\" {\n",[66,7344,7346],{"class":68,"line":7345},68,[66,7347,7348],{}," value = digitalocean_droplet.api_server.ipv4_address\n",[66,7350,7352],{"class":68,"line":7351},69,[66,7353,144],{},[20,7355,7356,7357,7360,7361,7364,7365,7368],{},"This defines a server, a firewall, and an output. Run ",[47,7358,7359],{},"terraform plan"," to see what it will create. Run ",[47,7362,7363],{},"terraform apply"," to create it. Run ",[47,7366,7367],{},"terraform destroy"," to tear it all down. The entire configuration lives in git — version controlled, reviewable, and reproducible.",[15,7370,7372],{"id":7371},"remote-state-is-non-negotiable","Remote State Is Non-Negotiable",[20,7374,7375],{},"Terraform tracks what it has created in a state file. By default, this file lives locally. This is fine for solo experimentation and a disaster for anything shared.",[20,7377,7378,7379,7381],{},"Local state means only one person can safely run Terraform at a time. If two team members run ",[47,7380,7363],{}," simultaneously, you get state corruption. If the machine holding the state file dies, you lose the ability to manage your infrastructure through Terraform.",[20,7383,7384],{},"Use remote state from day one. The configuration above uses DigitalOcean Spaces (S3-compatible) as a backend. AWS S3 with DynamoDB locking is the more common pattern for AWS deployments:",[57,7386,7388],{"className":7010,"code":7387,"language":7012,"meta":62,"style":62},"backend \"s3\" {\n bucket = \"my-company-terraform-state\"\n key = \"production/terraform.tfstate\"\n region = \"us-east-1\"\n dynamodb_table = \"terraform-state-lock\"\n encrypt = true\n}\n",[47,7389,7390,7395,7400,7404,7408,7413,7418],{"__ignoreMap":62},[66,7391,7392],{"class":68,"line":69},[66,7393,7394],{},"backend \"s3\" {\n",[66,7396,7397],{"class":68,"line":80},[66,7398,7399],{}," bucket = \"my-company-terraform-state\"\n",[66,7401,7402],{"class":68,"line":97},[66,7403,7071],{},[66,7405,7406],{"class":68,"line":128},[66,7407,7076],{},[66,7409,7410],{"class":68,"line":141},[66,7411,7412],{}," dynamodb_table = \"terraform-state-lock\"\n",[66,7414,7415],{"class":68,"line":259},[66,7416,7417],{}," encrypt = true\n",[66,7419,7420],{"class":68,"line":265},[66,7421,144],{},[20,7423,7424,7425,7428],{},"The DynamoDB table provides state locking — only one operation can modify state at a time. The ",[47,7426,7427],{},"encrypt = true"," ensures state is encrypted at rest, which matters because state files contain sensitive data including resource IDs, IPs, and sometimes passwords.",[15,7430,7432],{"id":7431},"modules-for-reusability","Modules for Reusability",[20,7434,7435],{},"As your infrastructure grows, you will duplicate patterns. Every environment needs a similar server configuration. Every service needs similar security groups. Terraform modules let you extract these patterns into reusable components.",[57,7437,7440],{"className":7438,"code":7439,"language":2905},[2903],"infrastructure/\n modules/\n web-server/\n main.tf\n variables.tf\n outputs.tf\n environments/\n production/\n main.tf\n staging/\n main.tf\n",[47,7441,7439],{"__ignoreMap":62},[20,7443,7444,7445,7448],{},"Your environment-specific ",[47,7446,7447],{},"main.tf"," calls the module:",[57,7450,7452],{"className":7010,"code":7451,"language":7012,"meta":62,"style":62},"module \"api_server\" {\n source = \"../../modules/web-server\"\n environment = \"production\"\n size = \"s-2vcpu-4gb\"\n ssh_keys = [var.ssh_key_fingerprint]\n}\n",[47,7453,7454,7459,7464,7469,7473,7477],{"__ignoreMap":62},[66,7455,7456],{"class":68,"line":69},[66,7457,7458],{},"module \"api_server\" {\n",[66,7460,7461],{"class":68,"line":80},[66,7462,7463],{}," source = \"../../modules/web-server\"\n",[66,7465,7466],{"class":68,"line":97},[66,7467,7468],{}," environment = \"production\"\n",[66,7470,7471],{"class":68,"line":128},[66,7472,7181],{},[66,7474,7475],{"class":68,"line":141},[66,7476,7196],{},[66,7478,7479],{"class":68,"line":259},[66,7480,144],{},[20,7482,7483],{},"The same module, called with different variables, creates staging and production environments. When you need to update the server configuration, you update the module once and both environments stay in sync.",[15,7485,7487],{"id":7486},"the-plan-review-workflow","The Plan Review Workflow",[20,7489,7490,7491,7493],{},"The IaC equivalent of a pull request review is reviewing the Terraform plan before applying. Never run ",[47,7492,7363],{}," in production without reviewing the plan output first. The plan shows exactly what will be created, modified, or destroyed.",[20,7495,7496,7497,7499],{},"In CI, run ",[47,7498,7359],{}," on every pull request and post the plan output as a PR comment. Tools like Atlantis automate this workflow. Reviewers can see the infrastructure changes alongside the code changes. Infrastructure modifications require the same scrutiny as application code modifications.",[57,7501,7503],{"className":1724,"code":7502,"language":1726,"meta":62,"style":62},"# GitHub Actions step for plan output\n- name: Terraform Plan\n run: terraform plan -out=tfplan\n env:\n TF_VAR_do_token: ${{ secrets.DO_TOKEN }}\n\n- name: Comment Plan Output\n uses: actions/github-script@v7\n with:\n script: |\n const plan = require('fs').readFileSync('plan-output.txt', 'utf8');\n github.rest.issues.createComment({\n issue_number: context.issue.number,\n owner: context.repo.owner,\n repo: context.repo.repo,\n body: '## Terraform Plan\\n```\\n' + plan + '\\n```'\n });\n",[47,7504,7505,7510,7521,7530,7536,7546,7550,7561,7570,7576,7585,7590,7595,7600,7605,7610,7615],{"__ignoreMap":62},[66,7506,7507],{"class":68,"line":69},[66,7508,7509],{"class":582},"# GitHub Actions step for plan output\n",[66,7511,7512,7514,7516,7518],{"class":68,"line":80},[66,7513,2353],{"class":76},[66,7515,1733],{"class":213},[66,7517,87],{"class":76},[66,7519,7520],{"class":90},"Terraform Plan\n",[66,7522,7523,7525,7527],{"class":68,"line":97},[66,7524,2277],{"class":213},[66,7526,87],{"class":76},[66,7528,7529],{"class":90},"terraform plan -out=tfplan\n",[66,7531,7532,7534],{"class":68,"line":128},[66,7533,1849],{"class":213},[66,7535,1750],{"class":76},[66,7537,7538,7541,7543],{"class":68,"line":141},[66,7539,7540],{"class":213}," TF_VAR_do_token",[66,7542,87],{"class":76},[66,7544,7545],{"class":90},"${{ secrets.DO_TOKEN }}\n",[66,7547,7548],{"class":68,"line":259},[66,7549,459],{"emptyLinePlaceholder":458},[66,7551,7552,7554,7556,7558],{"class":68,"line":265},[66,7553,2353],{"class":76},[66,7555,1733],{"class":213},[66,7557,87],{"class":76},[66,7559,7560],{"class":90},"Comment Plan Output\n",[66,7562,7563,7565,7567],{"class":68,"line":450},[66,7564,2365],{"class":213},[66,7566,87],{"class":76},[66,7568,7569],{"class":90},"actions/github-script@v7\n",[66,7571,7572,7574],{"class":68,"line":455},[66,7573,2208],{"class":213},[66,7575,1750],{"class":76},[66,7577,7578,7581,7583],{"class":68,"line":462},[66,7579,7580],{"class":213}," script",[66,7582,87],{"class":76},[66,7584,2282],{"class":72},[66,7586,7587],{"class":68,"line":470},[66,7588,7589],{"class":90}," const plan = require('fs').readFileSync('plan-output.txt', 'utf8');\n",[66,7591,7592],{"class":68,"line":491},[66,7593,7594],{"class":90}," github.rest.issues.createComment({\n",[66,7596,7597],{"class":68,"line":1175},[66,7598,7599],{"class":90}," issue_number: context.issue.number,\n",[66,7601,7602],{"class":68,"line":1183},[66,7603,7604],{"class":90}," owner: context.repo.owner,\n",[66,7606,7607],{"class":68,"line":1191},[66,7608,7609],{"class":90}," repo: context.repo.repo,\n",[66,7611,7612],{"class":68,"line":1846},[66,7613,7614],{"class":90}," body: '## Terraform Plan\\n```\\n' + plan + '\\n```'\n",[66,7616,7617],{"class":68,"line":1854},[66,7618,7619],{"class":90}," });\n",[15,7621,7623],{"id":7622},"starting-small","Starting Small",[20,7625,7626],{},"You do not need to migrate all your infrastructure to Terraform overnight. Start with the next thing you would have clicked through a console to create. A new S3 bucket, a DNS record, a security group rule. Define it in Terraform, apply it, check it into git.",[20,7628,7629],{},"Incremental adoption is fine. Gradually expand your coverage. Import existing resources into Terraform state when it makes sense. The goal is that eventually, your production environment can be described completely in code and recreated from scratch with a single command.",[20,7631,7632],{},"The day you need that — and eventually, every production system needs that — you will be grateful you started early.",[40,7634],{},[20,7636,7637,7638,1138],{},"Want help setting up Terraform for your infrastructure? Let's design an IaC strategy that fits your stack. Book a session at ",[690,7639,692],{"href":692,"rel":7640},[694],[40,7642],{},[15,7644,702],{"id":701},[304,7646,7647,7651,7657,7661],{},[307,7648,7649],{},[690,7650,2799],{"href":2798},[307,7652,7653],{},[690,7654,7656],{"href":7655},"/blog/environment-variables-guide","Environment Variables Done Right: Secrets, Config, and Everything In Between",[307,7658,7659],{},[690,7660,3329],{"href":3328},[307,7662,7663],{},[690,7664,6928],{"href":6927},[730,7666,7667],{},"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}",{"title":62,"searchDepth":97,"depth":97,"links":7669},[7670,7671,7672,7673,7674,7675,7676,7677],{"id":6980,"depth":80,"text":6981},{"id":6993,"depth":80,"text":6994},{"id":7003,"depth":80,"text":7004},{"id":7371,"depth":80,"text":7372},{"id":7431,"depth":80,"text":7432},{"id":7486,"depth":80,"text":7487},{"id":7622,"depth":80,"text":7623},{"id":701,"depth":80,"text":702},"A practical guide to Infrastructure as Code with Terraform — versioning, modules, remote state, and why treating config as code is non-negotiable.",[7680,7681,2835],"infrastructure as code","Terraform",{},"/blog/infrastructure-as-code-guide",{"title":6965,"description":7678},"blog/infrastructure-as-code-guide",[7687,7681,2835,7688],"Infrastructure as Code","Cloud","GdCO0MQvxvlhAC7eKw3vuynAImeDhZwV1TAGO5U75Gs",[7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7958,7959,7960,7961,7962,7963,7964,7965,7966,7967,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8006,8007,8008,8009,8010,8011,8012,8013,8014,8015,8016,8017,8018,8019,8020,8021,8022,8023,8024,8025,8026,8027,8028,8029,8030,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8062,8063,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8117,8118,8119,8120,8121,8122,8123,8124,8125,8126,8127,8128,8129,8130,8131,8132,8133,8134,8135,8136,8137,8138,8139,8140,8141,8142,8143,8144,8145,8146,8147,8148,8149,8150,8151,8152,8153,8154,8155,8156,8157,8158,8159,8160,8161,8162,8164,8165,8166,8167,8168,8169,8170,8171,8172,8173,8174,8175,8176,8177,8178,8179,8180,8181,8182,8183,8184,8185,8186,8187,8188,8189,8190,8191,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207,8208,8209,8210,8211,8212,8213,8214,8215,8216,8217,8218,8219,8220,8221,8222,8223,8224,8225,8226,8227,8228,8229,8230,8231,8232,8233,8234,8235,8236,8237,8238,8239,8240,8241,8242,8243,8244,8245,8246,8247,8248,8249,8250,8251,8252,8253,8254,8255,8256,8257,8258,8259,8260,8261,8262,8263,8264,8265,8266,8267,8268,8269,8270,8271,8272,8273,8274,8275,8276,8277,8278,8279,8280,8281,8282,8283,8284,8285,8286,8287,8288,8289,8290,8291,8292,8293,8294,8295,8296,8297,8298,8299,8300,8301,8302,8303,8304,8305,8306,8307,8308,8309,8310,8311,8312,8313,8314,8315,8316,8317,8318,8319,8320,8321,8322,8323,8324,8325,8326,8327,8328,8329,8330,8331,8332],{"category":1499},{"category":4459},{"category":1673},{"category":745},{"category":972},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":1673},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":4112},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":7730},"Security",{"category":7730},{"category":972},{"category":972},{"category":4459},{"category":7730},{"category":4459},{"category":4112},{"category":7730},{"category":745},{"category":972},{"category":2835},{"category":1673},{"category":4459},{"category":745},{"category":4112},{"category":745},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":2835},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":745},{"category":1685},{"category":1673},{"category":1673},{"category":972},{"category":4112},{"category":972},{"category":745},{"category":745},{"category":972},{"category":745},{"category":4112},{"category":745},{"category":2835},{"category":2835},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":1673},{"category":4112},{"category":972},{"category":2835},{"category":2835},{"category":2835},{"category":4459},{"category":745},{"category":745},{"category":4459},{"category":1499},{"category":1673},{"category":2835},{"category":2835},{"category":7730},{"category":2835},{"category":972},{"category":1673},{"category":4459},{"category":745},{"category":4459},{"category":4112},{"category":4459},{"category":4112},{"category":7730},{"category":4459},{"category":4459},{"category":745},{"category":972},{"category":745},{"category":1499},{"category":745},{"category":745},{"category":745},{"category":745},{"category":972},{"category":972},{"category":4459},{"category":1499},{"category":7730},{"category":4112},{"category":7730},{"category":1499},{"category":745},{"category":745},{"category":2835},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":2835},{"category":745},{"category":745},{"category":4459},{"category":4459},{"category":7730},{"category":4112},{"category":4112},{"category":1685},{"category":1685},{"category":1685},{"category":972},{"category":745},{"category":2835},{"category":4112},{"category":4459},{"category":4459},{"category":2835},{"category":4112},{"category":4112},{"category":1499},{"category":745},{"category":4459},{"category":4459},{"category":745},{"category":4459},{"category":2835},{"category":2835},{"category":4459},{"category":7730},{"category":4459},{"category":4112},{"category":7730},{"category":4112},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":7730},{"category":745},{"category":2835},{"category":2835},{"category":972},{"category":745},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":4112},{"category":4112},{"category":4112},{"category":745},{"category":4459},{"category":4459},{"category":4459},{"category":2835},{"category":972},{"category":4459},{"category":4459},{"category":745},{"category":4459},{"category":745},{"category":1499},{"category":4459},{"category":972},{"category":972},{"category":745},{"category":745},{"category":1673},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":745},{"category":2835},{"category":2835},{"category":2835},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":4459},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":972},{"category":972},{"category":4459},{"category":745},{"category":1499},{"category":4112},{"category":1685},{"category":4459},{"category":4459},{"category":7730},{"category":745},{"category":4459},{"category":4459},{"category":2835},{"category":4459},{"category":1499},{"category":2835},{"category":2835},{"category":7730},{"category":745},{"category":745},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":1685},{"category":4459},{"category":4112},{"category":745},{"category":745},{"category":4459},{"category":2835},{"category":4459},{"category":4459},{"category":4459},{"category":1499},{"category":4459},{"category":4459},{"category":745},{"category":4459},{"category":745},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":1673},{"category":1673},{"category":745},{"category":4459},{"category":2835},{"category":2835},{"category":4459},{"category":745},{"category":4459},{"category":4459},{"category":1673},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":745},{"category":7730},{"category":745},{"category":745},{"category":1499},{"category":745},{"category":1499},{"category":1499},{"category":7730},{"category":4112},{"category":745},{"category":4112},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":745},{"category":972},{"category":745},{"category":745},{"category":4459},{"category":4112},{"category":1673},{"category":1673},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":972},{"category":745},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":1499},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":745},{"category":4112},{"category":4459},{"category":972},{"category":1673},{"category":4459},{"category":972},{"category":7730},{"category":4459},{"category":7730},{"category":745},{"category":2835},{"category":4459},{"category":4459},{"category":745},{"category":4459},{"category":4112},{"category":4459},{"category":4459},{"category":745},{"category":972},{"category":745},{"category":745},{"category":745},{"category":745},{"category":972},{"category":745},{"category":745},{"category":972},{"category":2835},{"category":745},{"category":1673},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":4459},{"category":4459},{"category":4459},{"category":1673},{"category":745},{"category":745},{"category":4112},{"category":1499},{"category":745},{"category":4459},{"category":745},{"category":4112},{"category":972},{"category":972},{"category":1499},{"category":1499},{"category":4459},{"category":972},{"category":7730},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":745},{"category":745},{"category":4112},{"category":745},{"category":745},{"category":745},{"category":8163},"Programming",{"category":745},{"category":745},{"category":4112},{"category":4112},{"category":745},{"category":745},{"category":972},{"category":7730},{"category":745},{"category":972},{"category":745},{"category":745},{"category":745},{"category":745},{"category":2835},{"category":4112},{"category":972},{"category":972},{"category":745},{"category":745},{"category":972},{"category":745},{"category":7730},{"category":972},{"category":745},{"category":745},{"category":4112},{"category":4112},{"category":4459},{"category":972},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":1499},{"category":4459},{"category":2835},{"category":7730},{"category":7730},{"category":7730},{"category":7730},{"category":7730},{"category":7730},{"category":4459},{"category":745},{"category":2835},{"category":4112},{"category":2835},{"category":4112},{"category":745},{"category":1499},{"category":4459},{"category":4112},{"category":1499},{"category":4459},{"category":4459},{"category":4459},{"category":4112},{"category":4112},{"category":4112},{"category":972},{"category":972},{"category":972},{"category":4112},{"category":4112},{"category":972},{"category":972},{"category":972},{"category":4459},{"category":7730},{"category":745},{"category":2835},{"category":745},{"category":4459},{"category":972},{"category":972},{"category":4459},{"category":4459},{"category":4112},{"category":745},{"category":4112},{"category":4112},{"category":4112},{"category":1499},{"category":745},{"category":4459},{"category":4459},{"category":972},{"category":972},{"category":4112},{"category":745},{"category":1685},{"category":4112},{"category":1685},{"category":972},{"category":4459},{"category":4112},{"category":4459},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":4459},{"category":1673},{"category":1673},{"category":2835},{"category":4459},{"category":4459},{"category":4459},{"category":4459},{"category":745},{"category":745},{"category":1499},{"category":745},{"category":7730},{"category":4112},{"category":1499},{"category":1499},{"category":745},{"category":745},{"category":1499},{"category":1499},{"category":1499},{"category":7730},{"category":745},{"category":745},{"category":972},{"category":745},{"category":4112},{"category":4459},{"category":4459},{"category":4112},{"category":4459},{"category":4459},{"category":4112},{"category":4459},{"category":745},{"category":4459},{"category":7730},{"category":4459},{"category":4459},{"category":4459},{"category":2835},{"category":2835},{"category":7730},1772951194520]