[{"data":1,"prerenderedAt":11875},["ShallowReactive",2],{"writing-why-i-built-a-new-vite-env-plugin":3,"all-writings":1469},{"id":4,"title":5,"body":6,"category":1459,"date":1460,"description":1461,"draft":1462,"extension":1463,"meta":1464,"navigation":378,"path":1465,"seo":1466,"sitemap":378,"stem":1467,"__hash__":1468},"writings\u002Fwritings\u002Fwhy-i-built-a-new-vite-env-plugin.md","Why I Built a New Vite Env Plugin",{"type":7,"value":8,"toc":1446},"minimark",[9,13,97,104,107,112,123,127,134,174,177,181,184,188,248,255,259,262,279,297,300,304,318,649,727,730,739,745,870,883,895,919,923,926,1110,1117,1121,1261,1273,1277,1283,1300,1324,1328,1387,1397,1401,1420,1439,1442],[10,11,12],"p",{},"This is what environment variables look like in a Vite project:",[14,15,20],"pre",{"className":16,"code":17,"language":18,"meta":19,"style":19},"language-ts shiki shiki-themes vitesse-light vitesse-dark","import.meta.env.VITE_PORT     \u002F\u002F \"5173\" — string, not number\nimport.meta.env.VITE_DARK     \u002F\u002F \"true\" — string, not boolean\nimport.meta.env.VITE_API_URL  \u002F\u002F string | undefined — no validation\n","ts","",[21,22,23,55,76],"code",{"__ignoreMap":19},[24,25,28,32,36,40,42,46,48,51],"span",{"class":26,"line":27},"line",1,[24,29,31],{"class":30},"sTPum","import",[24,33,35],{"class":34},"si6no",".",[24,37,39],{"class":38},"sHLBJ","meta",[24,41,35],{"class":34},[24,43,45],{"class":44},"s9nN2","env",[24,47,35],{"class":34},[24,49,50],{"class":44},"VITE_PORT",[24,52,54],{"class":53},"snYqZ","     \u002F\u002F \"5173\" — string, not number\n",[24,56,58,60,62,64,66,68,70,73],{"class":26,"line":57},2,[24,59,31],{"class":30},[24,61,35],{"class":34},[24,63,39],{"class":38},[24,65,35],{"class":34},[24,67,45],{"class":44},[24,69,35],{"class":34},[24,71,72],{"class":44},"VITE_DARK",[24,74,75],{"class":53},"     \u002F\u002F \"true\" — string, not boolean\n",[24,77,79,81,83,85,87,89,91,94],{"class":26,"line":78},3,[24,80,31],{"class":30},[24,82,35],{"class":34},[24,84,39],{"class":38},[24,86,35],{"class":34},[24,88,45],{"class":44},[24,90,35],{"class":34},[24,92,93],{"class":44},"VITE_API_URL",[24,95,96],{"class":53},"  \u002F\u002F string | undefined — no validation\n",[10,98,99,100,103],{},"Every value is a raw string. There's no validation, no server\u002Fclient boundary, no leak detection. The only way to get types is a ",[21,101,102],{},"vite-env.d.ts"," you write and maintain by hand.",[10,105,106],{},"Four problems. I built a plugin that fixes all of them.",[108,109,111],"h2",{"id":110},"problem-1-everything-is-a-string","Problem 1: Everything is a string",[10,113,114,115,118,119,122],{},"You coerce values yourself. A forgotten ",[21,116,117],{},"Number()"," or a ",[21,120,121],{},"=== true"," on a string is a quiet bug that passes every check until it doesn't.",[108,124,126],{"id":125},"problem-2-no-serverclient-boundary","Problem 2: No server\u002Fclient boundary",[10,128,129,130,133],{},"Variables prefixed with ",[21,131,132],{},"VITE_"," go to the client. Everything else stays server-side. That's the convention. There's no enforcement, no explicit split, no warning if you cross the line.",[14,135,137],{"className":16,"code":136,"language":18,"meta":19,"style":19},"\u002F\u002F shared\u002Fconfig.ts — imported in both server and client code\nexport const db = process.env.DATABASE_URL \u002F\u002F silently bundled\n",[21,138,139,144],{"__ignoreMap":19},[24,140,141],{"class":26,"line":27},[24,142,143],{"class":53},"\u002F\u002F shared\u002Fconfig.ts — imported in both server and client code\n",[24,145,146,149,153,156,159,162,164,166,168,171],{"class":26,"line":57},[24,147,148],{"class":30},"export",[24,150,152],{"class":151},"s5TCs"," const ",[24,154,155],{"class":44},"db",[24,157,158],{"class":34}," =",[24,160,161],{"class":44}," process",[24,163,35],{"class":34},[24,165,45],{"class":44},[24,167,35],{"class":34},[24,169,170],{"class":44},"DATABASE_URL",[24,172,173],{"class":53}," \u002F\u002F silently bundled\n",[10,175,176],{},"If server and client code share a module, secrets travel with it.",[108,178,180],{"id":179},"problem-3-no-leak-detection","Problem 3: No leak detection",[10,182,183],{},"Even careful code can leak. Bundlers inline values. After tree-shaking and minification, the literal string value of a server secret can appear inside a client chunk — no import reference, just the raw value embedded in compiled output. Nothing checks for this.",[108,185,187],{"id":186},"problem-4-manual-type-maintenance","Problem 4: Manual type maintenance",[14,189,191],{"className":16,"code":190,"language":18,"meta":19,"style":19},"\u002F\u002F vite-env.d.ts — written and updated by hand\ninterface ImportMetaEnv {\n  readonly VITE_API_URL: string\n  readonly VITE_PORT: string\n  \u002F\u002F someone added VITE_FEATURE_FLAG last week and forgot this file\n}\n",[21,192,193,198,210,224,236,242],{"__ignoreMap":19},[24,194,195],{"class":26,"line":27},[24,196,197],{"class":53},"\u002F\u002F vite-env.d.ts — written and updated by hand\n",[24,199,200,203,207],{"class":26,"line":57},[24,201,202],{"class":151},"interface",[24,204,206],{"class":205},"s_NWU"," ImportMetaEnv",[24,208,209],{"class":34}," {\n",[24,211,212,215,218,221],{"class":26,"line":78},[24,213,214],{"class":151},"  readonly",[24,216,217],{"class":44}," VITE_API_URL",[24,219,220],{"class":34},": ",[24,222,223],{"class":205},"string\n",[24,225,227,229,232,234],{"class":26,"line":226},4,[24,228,214],{"class":151},[24,230,231],{"class":44}," VITE_PORT",[24,233,220],{"class":34},[24,235,223],{"class":205},[24,237,239],{"class":26,"line":238},5,[24,240,241],{"class":53},"  \u002F\u002F someone added VITE_FEATURE_FLAG last week and forgot this file\n",[24,243,245],{"class":26,"line":244},6,[24,246,247],{"class":34},"}\n",[10,249,250,251,254],{},"These drift. The variable is in ",[21,252,253],{},".env",". TypeScript doesn't complain. The mismatch goes unnoticed.",[108,256,258],{"id":257},"what-already-exists","What already exists",[10,260,261],{},"Two tools address parts of this.",[10,263,264,274,275,278],{},[265,266,267],"strong",{},[268,269,273],"a",{"href":270,"rel":271},"https:\u002F\u002Fgithub.com\u002FJulien-R44\u002Fvite-plugin-validate-env",[272],"nofollow","@julr\u002Fvite-plugin-validate-env"," validates env at build time and injects values into ",[21,276,277],{},"import.meta.env",". Supports Standard Schema (Zod, Valibot, ArkType) and a lightweight built-in validator. Zero runtime overhead. It does exactly what it promises — validation. It doesn't split server\u002Fclient variables, doesn't provide virtual modules, and doesn't detect leaks.",[10,280,281,288,289,292,293,296],{},[265,282,283],{},[268,284,287],{"href":285,"rel":286},"https:\u002F\u002Fgithub.com\u002Ft3-oss\u002Ft3-env",[272],"@t3-oss\u002Fenv-core"," validates at import time and provides runtime server\u002Fclient protection via a Proxy. Platform presets for Vercel, Railway, Netlify, and others. The ",[21,290,291],{},"extends"," system works well for monorepos. The trade-offs: ",[21,294,295],{},"runtimeEnv"," requires listing every variable twice, there's no build-time leak detection, and it's framework-agnostic — it can't hook into Vite's build pipeline.",[10,298,299],{},"Both are good tools. Neither solves all four problems for Vite.",[108,301,303],{"id":302},"one-file-everything-derived","One file, everything derived",[10,305,306,313,314,317],{},[268,307,310],{"href":308,"rel":309},"https:\u002F\u002Fgithub.com\u002Fpyyupsk\u002Fvite-env",[272],[21,311,312],{},"@vite-env\u002Fcore",". One ",[21,315,316],{},"env.ts"," file. The plugin handles validation, virtual modules, type generation, and leak detection from it.",[14,319,321],{"className":16,"code":320,"language":18,"meta":19,"style":19},"\u002F\u002F env.ts\nimport { defineEnv } from '@vite-env\u002Fcore'\nimport { z } from 'zod'\n\nexport default defineEnv({\n  server: {\n    DATABASE_URL: z.url(),\n    JWT_SECRET: z.string().min(32),\n    DB_POOL_SIZE: z.coerce.number().int().default(10),\n  },\n  client: {\n    VITE_API_URL: z.url(),\n    VITE_APP_NAME: z.string().min(1),\n    VITE_DEBUG: z.stringbool().default(false),\n    VITE_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),\n  },\n})\n",[21,322,323,328,354,374,380,393,401,420,451,488,494,502,518,543,569,638,643],{"__ignoreMap":19},[24,324,325],{"class":26,"line":27},[24,326,327],{"class":53},"\u002F\u002F env.ts\n",[24,329,330,332,335,338,341,344,348,351],{"class":26,"line":57},[24,331,31],{"class":30},[24,333,334],{"class":34}," {",[24,336,337],{"class":44}," defineEnv",[24,339,340],{"class":34}," }",[24,342,343],{"class":30}," from",[24,345,347],{"class":346},"scnC2"," '",[24,349,312],{"class":350},"spP0B",[24,352,353],{"class":346},"'\n",[24,355,356,358,360,363,365,367,369,372],{"class":26,"line":78},[24,357,31],{"class":30},[24,359,334],{"class":34},[24,361,362],{"class":44}," z",[24,364,340],{"class":34},[24,366,343],{"class":30},[24,368,347],{"class":346},[24,370,371],{"class":350},"zod",[24,373,353],{"class":346},[24,375,376],{"class":26,"line":226},[24,377,379],{"emptyLinePlaceholder":378},true,"\n",[24,381,382,384,387,390],{"class":26,"line":238},[24,383,148],{"class":30},[24,385,386],{"class":30}," default",[24,388,337],{"class":389},"s_xSY",[24,391,392],{"class":34},"({\n",[24,394,395,398],{"class":26,"line":244},[24,396,397],{"class":38},"  server",[24,399,400],{"class":34},": {\n",[24,402,404,407,409,412,414,417],{"class":26,"line":403},7,[24,405,406],{"class":38},"    DATABASE_URL",[24,408,220],{"class":34},[24,410,411],{"class":44},"z",[24,413,35],{"class":34},[24,415,416],{"class":389},"url",[24,418,419],{"class":34},"(),\n",[24,421,423,426,428,430,432,435,438,441,444,448],{"class":26,"line":422},8,[24,424,425],{"class":38},"    JWT_SECRET",[24,427,220],{"class":34},[24,429,411],{"class":44},[24,431,35],{"class":34},[24,433,434],{"class":389},"string",[24,436,437],{"class":34},"().",[24,439,440],{"class":389},"min",[24,442,443],{"class":34},"(",[24,445,447],{"class":446},"sqbOQ","32",[24,449,450],{"class":34},"),\n",[24,452,454,457,459,461,463,466,468,471,473,476,478,481,483,486],{"class":26,"line":453},9,[24,455,456],{"class":38},"    DB_POOL_SIZE",[24,458,220],{"class":34},[24,460,411],{"class":44},[24,462,35],{"class":34},[24,464,465],{"class":44},"coerce",[24,467,35],{"class":34},[24,469,470],{"class":389},"number",[24,472,437],{"class":34},[24,474,475],{"class":389},"int",[24,477,437],{"class":34},[24,479,480],{"class":389},"default",[24,482,443],{"class":34},[24,484,485],{"class":446},"10",[24,487,450],{"class":34},[24,489,491],{"class":26,"line":490},10,[24,492,493],{"class":34},"  },\n",[24,495,497,500],{"class":26,"line":496},11,[24,498,499],{"class":38},"  client",[24,501,400],{"class":34},[24,503,505,508,510,512,514,516],{"class":26,"line":504},12,[24,506,507],{"class":38},"    VITE_API_URL",[24,509,220],{"class":34},[24,511,411],{"class":44},[24,513,35],{"class":34},[24,515,416],{"class":389},[24,517,419],{"class":34},[24,519,521,524,526,528,530,532,534,536,538,541],{"class":26,"line":520},13,[24,522,523],{"class":38},"    VITE_APP_NAME",[24,525,220],{"class":34},[24,527,411],{"class":44},[24,529,35],{"class":34},[24,531,434],{"class":389},[24,533,437],{"class":34},[24,535,440],{"class":389},[24,537,443],{"class":34},[24,539,540],{"class":446},"1",[24,542,450],{"class":34},[24,544,546,549,551,553,555,558,560,562,564,567],{"class":26,"line":545},14,[24,547,548],{"class":38},"    VITE_DEBUG",[24,550,220],{"class":34},[24,552,411],{"class":44},[24,554,35],{"class":34},[24,556,557],{"class":389},"stringbool",[24,559,437],{"class":34},[24,561,480],{"class":389},[24,563,443],{"class":34},[24,565,566],{"class":30},"false",[24,568,450],{"class":34},[24,570,572,575,577,579,581,584,587,590,593,595,598,600,603,605,607,609,612,614,616,618,621,623,626,628,630,632,634,636],{"class":26,"line":571},15,[24,573,574],{"class":38},"    VITE_LOG_LEVEL",[24,576,220],{"class":34},[24,578,411],{"class":44},[24,580,35],{"class":34},[24,582,583],{"class":389},"enum",[24,585,586],{"class":34},"([",[24,588,589],{"class":346},"'",[24,591,592],{"class":350},"debug",[24,594,589],{"class":346},[24,596,597],{"class":34},", ",[24,599,589],{"class":346},[24,601,602],{"class":350},"info",[24,604,589],{"class":346},[24,606,597],{"class":34},[24,608,589],{"class":346},[24,610,611],{"class":350},"warn",[24,613,589],{"class":346},[24,615,597],{"class":34},[24,617,589],{"class":346},[24,619,620],{"class":350},"error",[24,622,589],{"class":346},[24,624,625],{"class":34},"]).",[24,627,480],{"class":389},[24,629,443],{"class":34},[24,631,589],{"class":346},[24,633,602],{"class":350},[24,635,589],{"class":346},[24,637,450],{"class":34},[24,639,641],{"class":26,"line":640},16,[24,642,493],{"class":34},[24,644,646],{"class":26,"line":645},17,[24,647,648],{"class":34},"})\n",[14,650,652],{"className":16,"code":651,"language":18,"meta":19,"style":19},"\u002F\u002F vite.config.ts\nimport ViteEnv from '@vite-env\u002Fcore\u002Fplugin'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n  plugins: [ViteEnv()],\n})\n",[21,653,654,659,675,695,699,709,723],{"__ignoreMap":19},[24,655,656],{"class":26,"line":27},[24,657,658],{"class":53},"\u002F\u002F vite.config.ts\n",[24,660,661,663,666,668,670,673],{"class":26,"line":57},[24,662,31],{"class":30},[24,664,665],{"class":44}," ViteEnv",[24,667,343],{"class":30},[24,669,347],{"class":346},[24,671,672],{"class":350},"@vite-env\u002Fcore\u002Fplugin",[24,674,353],{"class":346},[24,676,677,679,681,684,686,688,690,693],{"class":26,"line":78},[24,678,31],{"class":30},[24,680,334],{"class":34},[24,682,683],{"class":44}," defineConfig",[24,685,340],{"class":34},[24,687,343],{"class":30},[24,689,347],{"class":346},[24,691,692],{"class":350},"vite",[24,694,353],{"class":346},[24,696,697],{"class":26,"line":226},[24,698,379],{"emptyLinePlaceholder":378},[24,700,701,703,705,707],{"class":26,"line":238},[24,702,148],{"class":30},[24,704,386],{"class":30},[24,706,683],{"class":389},[24,708,392],{"class":34},[24,710,711,714,717,720],{"class":26,"line":244},[24,712,713],{"class":38},"  plugins",[24,715,716],{"class":34},": [",[24,718,719],{"class":389},"ViteEnv",[24,721,722],{"class":34},"()],\n",[24,724,725],{"class":26,"line":403},[24,726,648],{"class":34},[10,728,729],{},"That's the entire setup.",[10,731,732,735,736,738],{},[265,733,734],{},"Validation"," runs at build start. Missing or malformed variables fail immediately with a list of every problem at once. During dev, ",[21,737,253],{}," changes revalidate — terminal warning, no crash.",[10,740,741,744],{},[265,742,743],{},"Virtual modules"," enforce the split:",[14,746,748],{"className":16,"code":747,"language":18,"meta":19,"style":19},"\u002F\u002F Client code\nimport { env } from 'virtual:env\u002Fclient'\nenv.VITE_API_URL   \u002F\u002F string\nenv.VITE_DEBUG     \u002F\u002F boolean — coerced, not \"true\"\nenv.DATABASE_URL   \u002F\u002F TypeScript error — doesn't exist here\n\n\u002F\u002F Server\u002FSSR code\nimport { env } from 'virtual:env\u002Fserver'\nenv.DATABASE_URL   \u002F\u002F string\nenv.JWT_SECRET     \u002F\u002F string\nenv.VITE_API_URL   \u002F\u002F also available\n",[21,749,750,755,775,786,798,809,813,818,837,847,859],{"__ignoreMap":19},[24,751,752],{"class":26,"line":27},[24,753,754],{"class":53},"\u002F\u002F Client code\n",[24,756,757,759,761,764,766,768,770,773],{"class":26,"line":57},[24,758,31],{"class":30},[24,760,334],{"class":34},[24,762,763],{"class":44}," env",[24,765,340],{"class":34},[24,767,343],{"class":30},[24,769,347],{"class":346},[24,771,772],{"class":350},"virtual:env\u002Fclient",[24,774,353],{"class":346},[24,776,777,779,781,783],{"class":26,"line":78},[24,778,45],{"class":44},[24,780,35],{"class":34},[24,782,93],{"class":44},[24,784,785],{"class":53},"   \u002F\u002F string\n",[24,787,788,790,792,795],{"class":26,"line":226},[24,789,45],{"class":44},[24,791,35],{"class":34},[24,793,794],{"class":44},"VITE_DEBUG",[24,796,797],{"class":53},"     \u002F\u002F boolean — coerced, not \"true\"\n",[24,799,800,802,804,806],{"class":26,"line":238},[24,801,45],{"class":44},[24,803,35],{"class":34},[24,805,170],{"class":44},[24,807,808],{"class":53},"   \u002F\u002F TypeScript error — doesn't exist here\n",[24,810,811],{"class":26,"line":244},[24,812,379],{"emptyLinePlaceholder":378},[24,814,815],{"class":26,"line":403},[24,816,817],{"class":53},"\u002F\u002F Server\u002FSSR code\n",[24,819,820,822,824,826,828,830,832,835],{"class":26,"line":422},[24,821,31],{"class":30},[24,823,334],{"class":34},[24,825,763],{"class":44},[24,827,340],{"class":34},[24,829,343],{"class":30},[24,831,347],{"class":346},[24,833,834],{"class":350},"virtual:env\u002Fserver",[24,836,353],{"class":346},[24,838,839,841,843,845],{"class":26,"line":453},[24,840,45],{"class":44},[24,842,35],{"class":34},[24,844,170],{"class":44},[24,846,785],{"class":53},[24,848,849,851,853,856],{"class":26,"line":490},[24,850,45],{"class":44},[24,852,35],{"class":34},[24,854,855],{"class":44},"JWT_SECRET",[24,857,858],{"class":53},"     \u002F\u002F string\n",[24,860,861,863,865,867],{"class":26,"line":496},[24,862,45],{"class":44},[24,864,35],{"class":34},[24,866,93],{"class":44},[24,868,869],{"class":53},"   \u002F\u002F also available\n",[10,871,872,875,876,879,880,882],{},[265,873,874],{},"Leak detection"," scans every client chunk at ",[21,877,878],{},"generateBundle"," for the literal string values of server variables. If ",[21,881,170],{},"'s value appears anywhere in the browser bundle, the build fails with the chunk name.",[10,884,885,888,889,891,892,894],{},[265,886,887],{},"Type generation"," writes ",[21,890,102],{}," on every build start. Add a variable to ",[21,893,316],{},", the declaration file updates. Nothing to maintain by hand.",[10,896,897,900,901,903,904,907,908,911,912,915,916,918],{},[265,898,899],{},"Runtime access protection"," uses Vite 8's Environment API. If client code imports ",[21,902,834],{},", the plugin intercepts it during the build. Three modes: ",[21,905,906],{},"'error'"," (hard fail), ",[21,909,910],{},"'warn'"," (default — logs and exits with code 1), ",[21,913,914],{},"'stub'"," (returns a module that throws at access time, useful for isomorphic framework files). The default changes to ",[21,917,906],{}," in 1.0.0 — set it explicitly now if you're already using the plugin.",[108,920,922],{"id":921},"standard-schema","Standard Schema",[10,924,925],{},"If you prefer Valibot, ArkType, or any other Standard Schema validator:",[14,927,929],{"className":16,"code":928,"language":18,"meta":19,"style":19},"import { defineStandardEnv } from '@vite-env\u002Fcore'\nimport * as v from 'valibot'\n\nexport default defineStandardEnv({\n  server: {\n    DATABASE_URL: v.pipe(v.string(), v.url()),\n  },\n  client: {\n    VITE_API_URL: v.pipe(v.string(), v.url()),\n    VITE_APP_NAME: v.pipe(v.string(), v.minLength(1)),\n  },\n})\n",[21,930,931,950,972,976,986,992,1026,1030,1036,1066,1102,1106],{"__ignoreMap":19},[24,932,933,935,937,940,942,944,946,948],{"class":26,"line":27},[24,934,31],{"class":30},[24,936,334],{"class":34},[24,938,939],{"class":44}," defineStandardEnv",[24,941,340],{"class":34},[24,943,343],{"class":30},[24,945,347],{"class":346},[24,947,312],{"class":350},[24,949,353],{"class":346},[24,951,952,954,957,960,963,965,967,970],{"class":26,"line":57},[24,953,31],{"class":30},[24,955,956],{"class":151}," *",[24,958,959],{"class":30}," as",[24,961,962],{"class":44}," v",[24,964,343],{"class":30},[24,966,347],{"class":346},[24,968,969],{"class":350},"valibot",[24,971,353],{"class":346},[24,973,974],{"class":26,"line":78},[24,975,379],{"emptyLinePlaceholder":378},[24,977,978,980,982,984],{"class":26,"line":226},[24,979,148],{"class":30},[24,981,386],{"class":30},[24,983,939],{"class":389},[24,985,392],{"class":34},[24,987,988,990],{"class":26,"line":238},[24,989,397],{"class":38},[24,991,400],{"class":34},[24,993,994,996,998,1001,1003,1006,1008,1010,1012,1014,1017,1019,1021,1023],{"class":26,"line":244},[24,995,406],{"class":38},[24,997,220],{"class":34},[24,999,1000],{"class":44},"v",[24,1002,35],{"class":34},[24,1004,1005],{"class":389},"pipe",[24,1007,443],{"class":34},[24,1009,1000],{"class":44},[24,1011,35],{"class":34},[24,1013,434],{"class":389},[24,1015,1016],{"class":34},"(), ",[24,1018,1000],{"class":44},[24,1020,35],{"class":34},[24,1022,416],{"class":389},[24,1024,1025],{"class":34},"()),\n",[24,1027,1028],{"class":26,"line":403},[24,1029,493],{"class":34},[24,1031,1032,1034],{"class":26,"line":422},[24,1033,499],{"class":38},[24,1035,400],{"class":34},[24,1037,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058,1060,1062,1064],{"class":26,"line":453},[24,1039,507],{"class":38},[24,1041,220],{"class":34},[24,1043,1000],{"class":44},[24,1045,35],{"class":34},[24,1047,1005],{"class":389},[24,1049,443],{"class":34},[24,1051,1000],{"class":44},[24,1053,35],{"class":34},[24,1055,434],{"class":389},[24,1057,1016],{"class":34},[24,1059,1000],{"class":44},[24,1061,35],{"class":34},[24,1063,416],{"class":389},[24,1065,1025],{"class":34},[24,1067,1068,1070,1072,1074,1076,1078,1080,1082,1084,1086,1088,1090,1092,1095,1097,1099],{"class":26,"line":490},[24,1069,523],{"class":38},[24,1071,220],{"class":34},[24,1073,1000],{"class":44},[24,1075,35],{"class":34},[24,1077,1005],{"class":389},[24,1079,443],{"class":34},[24,1081,1000],{"class":44},[24,1083,35],{"class":34},[24,1085,434],{"class":389},[24,1087,1016],{"class":34},[24,1089,1000],{"class":44},[24,1091,35],{"class":34},[24,1093,1094],{"class":389},"minLength",[24,1096,443],{"class":34},[24,1098,540],{"class":446},[24,1100,1101],{"class":34},")),\n",[24,1103,1104],{"class":26,"line":496},[24,1105,493],{"class":34},[24,1107,1108],{"class":26,"line":504},[24,1109,648],{"class":34},[10,1111,1112,1113,1116],{},"Same plugin, same virtual modules, same leak detection. The generated ",[21,1114,1115],{},".d.ts"," types are less specific than with Zod — Standard Schema doesn't expose the same type introspection — but everything else works identically.",[108,1118,1120],{"id":1119},"platform-presets","Platform presets",[14,1122,1124],{"className":16,"code":1123,"language":18,"meta":19,"style":19},"import { defineEnv } from '@vite-env\u002Fcore'\nimport { vercel } from '@vite-env\u002Fcore\u002Fpresets'\nimport { z } from 'zod'\n\nexport default defineEnv({\n  presets: [vercel],\n  server: {\n    DATABASE_URL: z.url(),\n  },\n  client: {\n    VITE_API_URL: z.url(),\n  },\n})\n",[21,1125,1126,1144,1164,1182,1186,1196,1209,1215,1229,1233,1239,1253,1257],{"__ignoreMap":19},[24,1127,1128,1130,1132,1134,1136,1138,1140,1142],{"class":26,"line":27},[24,1129,31],{"class":30},[24,1131,334],{"class":34},[24,1133,337],{"class":44},[24,1135,340],{"class":34},[24,1137,343],{"class":30},[24,1139,347],{"class":346},[24,1141,312],{"class":350},[24,1143,353],{"class":346},[24,1145,1146,1148,1150,1153,1155,1157,1159,1162],{"class":26,"line":57},[24,1147,31],{"class":30},[24,1149,334],{"class":34},[24,1151,1152],{"class":44}," vercel",[24,1154,340],{"class":34},[24,1156,343],{"class":30},[24,1158,347],{"class":346},[24,1160,1161],{"class":350},"@vite-env\u002Fcore\u002Fpresets",[24,1163,353],{"class":346},[24,1165,1166,1168,1170,1172,1174,1176,1178,1180],{"class":26,"line":78},[24,1167,31],{"class":30},[24,1169,334],{"class":34},[24,1171,362],{"class":44},[24,1173,340],{"class":34},[24,1175,343],{"class":30},[24,1177,347],{"class":346},[24,1179,371],{"class":350},[24,1181,353],{"class":346},[24,1183,1184],{"class":26,"line":226},[24,1185,379],{"emptyLinePlaceholder":378},[24,1187,1188,1190,1192,1194],{"class":26,"line":238},[24,1189,148],{"class":30},[24,1191,386],{"class":30},[24,1193,337],{"class":389},[24,1195,392],{"class":34},[24,1197,1198,1201,1203,1206],{"class":26,"line":244},[24,1199,1200],{"class":38},"  presets",[24,1202,716],{"class":34},[24,1204,1205],{"class":44},"vercel",[24,1207,1208],{"class":34},"],\n",[24,1210,1211,1213],{"class":26,"line":403},[24,1212,397],{"class":38},[24,1214,400],{"class":34},[24,1216,1217,1219,1221,1223,1225,1227],{"class":26,"line":422},[24,1218,406],{"class":38},[24,1220,220],{"class":34},[24,1222,411],{"class":44},[24,1224,35],{"class":34},[24,1226,416],{"class":389},[24,1228,419],{"class":34},[24,1230,1231],{"class":26,"line":453},[24,1232,493],{"class":34},[24,1234,1235,1237],{"class":26,"line":490},[24,1236,499],{"class":38},[24,1238,400],{"class":34},[24,1240,1241,1243,1245,1247,1249,1251],{"class":26,"line":496},[24,1242,507],{"class":38},[24,1244,220],{"class":34},[24,1246,411],{"class":44},[24,1248,35],{"class":34},[24,1250,416],{"class":389},[24,1252,419],{"class":34},[24,1254,1255],{"class":26,"line":504},[24,1256,493],{"class":34},[24,1258,1259],{"class":26,"line":520},[24,1260,648],{"class":34},[10,1262,1263,1264,597,1266,597,1269,1272],{},"Available: ",[21,1265,1205],{},[21,1267,1268],{},"railway",[21,1270,1271],{},"netlify",". Your definitions take precedence over preset values.",[108,1274,1276],{"id":1275},"what-i-chose-not-to-do","What I chose not to do",[10,1278,1279,1282],{},[265,1280,1281],{},"No runtime Proxy."," t3-env throws at runtime when you access a server variable from the client. I chose build-time enforcement instead. Virtual modules and TypeScript catch it before the code runs. If you bypass TypeScript deliberately, there's no runtime throw — that's the trade-off, and I think it's the right one for a build tool.",[10,1284,1285,1291,1292,1295,1296,1299],{},[265,1286,1287,1288,1290],{},"No ",[21,1289,295],{}," mapping."," t3-env needs this because Next.js tree-shakes ",[21,1293,1294],{},"process.env"," access and requires explicit references to include variables in the bundle. Vite doesn't have this problem. The plugin calls ",[21,1297,1298],{},"loadEnv()"," directly and serves everything through virtual modules. You define a variable once.",[10,1301,1302,1305,1306,597,1309,597,1312,597,1315,597,1318,597,1320,1323],{},[265,1303,1304],{},"No framework adapters."," This is Vite-specific. It uses ",[21,1307,1308],{},"configResolved",[21,1310,1311],{},"buildStart",[21,1313,1314],{},"resolveId",[21,1316,1317],{},"load",[21,1319,878],{},[21,1321,1322],{},"configureServer",", and Vite 8's Environment API. If you're on Next.js or Nuxt without Vite, t3-env is the right tool.",[108,1325,1327],{"id":1326},"the-cli","The CLI",[14,1329,1333],{"className":1330,"code":1331,"language":1332,"meta":19,"style":19},"language-bash shiki shiki-themes vitesse-light vitesse-dark","# Validate without starting the dev server\nnpx vite-env check\n\n# Generate .env.example from your schema\nnpx vite-env generate\n\n# Regenerate vite-env.d.ts manually\nnpx vite-env types\n","bash",[21,1334,1335,1340,1351,1355,1360,1369,1373,1378],{"__ignoreMap":19},[24,1336,1337],{"class":26,"line":27},[24,1338,1339],{"class":53},"# Validate without starting the dev server\n",[24,1341,1342,1345,1348],{"class":26,"line":57},[24,1343,1344],{"class":389},"npx",[24,1346,1347],{"class":350}," vite-env",[24,1349,1350],{"class":350}," check\n",[24,1352,1353],{"class":26,"line":78},[24,1354,379],{"emptyLinePlaceholder":378},[24,1356,1357],{"class":26,"line":226},[24,1358,1359],{"class":53},"# Generate .env.example from your schema\n",[24,1361,1362,1364,1366],{"class":26,"line":238},[24,1363,1344],{"class":389},[24,1365,1347],{"class":350},[24,1367,1368],{"class":350}," generate\n",[24,1370,1371],{"class":26,"line":244},[24,1372,379],{"emptyLinePlaceholder":378},[24,1374,1375],{"class":26,"line":403},[24,1376,1377],{"class":53},"# Regenerate vite-env.d.ts manually\n",[24,1379,1380,1382,1384],{"class":26,"line":422},[24,1381,1344],{"class":389},[24,1383,1347],{"class":350},[24,1385,1386],{"class":350}," types\n",[10,1388,1389,1392,1393,1396],{},[21,1390,1391],{},"vite-env generate"," is the most useful one onboarding-wise. Run it once and new developers get a documented ",[21,1394,1395],{},".env.example"," with types, defaults, and required markers — all from the same schema.",[108,1398,1400],{"id":1399},"where-to-find-it","Where to find it",[1402,1403,1404,1412],"ul",{},[1405,1406,1407,1408],"li",{},"GitHub: ",[268,1409,1411],{"href":308,"rel":1410},[272],"pyyupsk\u002Fvite-env",[1405,1413,1414,1415],{},"Docs: ",[268,1416,1419],{"href":1417,"rel":1418},"https:\u002F\u002Fpyyupsk.github.io\u002Fvite-env\u002F",[272],"pyyupsk.github.io\u002Fvite-env",[14,1421,1423],{"className":1330,"code":1422,"language":1332,"meta":19,"style":19},"pnpm add @vite-env\u002Fcore zod\n",[21,1424,1425],{"__ignoreMap":19},[24,1426,1427,1430,1433,1436],{"class":26,"line":27},[24,1428,1429],{"class":389},"pnpm",[24,1431,1432],{"class":350}," add",[24,1434,1435],{"class":350}," @vite-env\u002Fcore",[24,1437,1438],{"class":350}," zod\n",[10,1440,1441],{},"If something doesn't work or the docs are unclear, open an issue.",[1443,1444,1445],"style",{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}",{"title":19,"searchDepth":57,"depth":57,"links":1447},[1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458],{"id":110,"depth":57,"text":111},{"id":125,"depth":57,"text":126},{"id":179,"depth":57,"text":180},{"id":186,"depth":57,"text":187},{"id":257,"depth":57,"text":258},{"id":302,"depth":57,"text":303},{"id":921,"depth":57,"text":922},{"id":1119,"depth":57,"text":1120},{"id":1275,"depth":57,"text":1276},{"id":1326,"depth":57,"text":1327},{"id":1399,"depth":57,"text":1400},"devlog","2026-04-08","The four problems with plain Vite environment variables — and the plugin I wrote to fix them.",false,"md",{},"\u002Fwritings\u002Fwhy-i-built-a-new-vite-env-plugin",{"title":5,"description":1461},"writings\u002Fwhy-i-built-a-new-vite-env-plugin","8tWTVrRgukKh7im82qZEolk01rywrhng3T3kLFTw_Rk",[1470,2621,3169,4024,4444,4835,6114,7108,7594,10180],{"id":4,"title":5,"body":1471,"category":1459,"date":1460,"description":1461,"draft":1462,"extension":1463,"meta":2619,"navigation":378,"path":1465,"seo":2620,"sitemap":378,"stem":1467,"__hash__":1468},{"type":7,"value":1472,"toc":2606},[1473,1475,1533,1537,1539,1541,1547,1549,1553,1583,1585,1587,1589,1591,1635,1639,1641,1643,1652,1663,1665,1667,1676,1940,2008,2010,2016,2020,2132,2140,2148,2162,2164,2166,2338,2342,2344,2478,2486,2488,2492,2502,2518,2520,2568,2574,2576,2588,2602,2604],[10,1474,12],{},[14,1476,1477],{"className":16,"code":17,"language":18,"meta":19,"style":19},[21,1478,1479,1497,1515],{"__ignoreMap":19},[24,1480,1481,1483,1485,1487,1489,1491,1493,1495],{"class":26,"line":27},[24,1482,31],{"class":30},[24,1484,35],{"class":34},[24,1486,39],{"class":38},[24,1488,35],{"class":34},[24,1490,45],{"class":44},[24,1492,35],{"class":34},[24,1494,50],{"class":44},[24,1496,54],{"class":53},[24,1498,1499,1501,1503,1505,1507,1509,1511,1513],{"class":26,"line":57},[24,1500,31],{"class":30},[24,1502,35],{"class":34},[24,1504,39],{"class":38},[24,1506,35],{"class":34},[24,1508,45],{"class":44},[24,1510,35],{"class":34},[24,1512,72],{"class":44},[24,1514,75],{"class":53},[24,1516,1517,1519,1521,1523,1525,1527,1529,1531],{"class":26,"line":78},[24,1518,31],{"class":30},[24,1520,35],{"class":34},[24,1522,39],{"class":38},[24,1524,35],{"class":34},[24,1526,45],{"class":44},[24,1528,35],{"class":34},[24,1530,93],{"class":44},[24,1532,96],{"class":53},[10,1534,99,1535,103],{},[21,1536,102],{},[10,1538,106],{},[108,1540,111],{"id":110},[10,1542,114,1543,118,1545,122],{},[21,1544,117],{},[21,1546,121],{},[108,1548,126],{"id":125},[10,1550,129,1551,133],{},[21,1552,132],{},[14,1554,1555],{"className":16,"code":136,"language":18,"meta":19,"style":19},[21,1556,1557,1561],{"__ignoreMap":19},[24,1558,1559],{"class":26,"line":27},[24,1560,143],{"class":53},[24,1562,1563,1565,1567,1569,1571,1573,1575,1577,1579,1581],{"class":26,"line":57},[24,1564,148],{"class":30},[24,1566,152],{"class":151},[24,1568,155],{"class":44},[24,1570,158],{"class":34},[24,1572,161],{"class":44},[24,1574,35],{"class":34},[24,1576,45],{"class":44},[24,1578,35],{"class":34},[24,1580,170],{"class":44},[24,1582,173],{"class":53},[10,1584,176],{},[108,1586,180],{"id":179},[10,1588,183],{},[108,1590,187],{"id":186},[14,1592,1593],{"className":16,"code":190,"language":18,"meta":19,"style":19},[21,1594,1595,1599,1607,1617,1627,1631],{"__ignoreMap":19},[24,1596,1597],{"class":26,"line":27},[24,1598,197],{"class":53},[24,1600,1601,1603,1605],{"class":26,"line":57},[24,1602,202],{"class":151},[24,1604,206],{"class":205},[24,1606,209],{"class":34},[24,1608,1609,1611,1613,1615],{"class":26,"line":78},[24,1610,214],{"class":151},[24,1612,217],{"class":44},[24,1614,220],{"class":34},[24,1616,223],{"class":205},[24,1618,1619,1621,1623,1625],{"class":26,"line":226},[24,1620,214],{"class":151},[24,1622,231],{"class":44},[24,1624,220],{"class":34},[24,1626,223],{"class":205},[24,1628,1629],{"class":26,"line":238},[24,1630,241],{"class":53},[24,1632,1633],{"class":26,"line":244},[24,1634,247],{"class":34},[10,1636,250,1637,254],{},[21,1638,253],{},[108,1640,258],{"id":257},[10,1642,261],{},[10,1644,1645,274,1650,278],{},[265,1646,1647],{},[268,1648,273],{"href":270,"rel":1649},[272],[21,1651,277],{},[10,1653,1654,288,1659,292,1661,296],{},[265,1655,1656],{},[268,1657,287],{"href":285,"rel":1658},[272],[21,1660,291],{},[21,1662,295],{},[10,1664,299],{},[108,1666,303],{"id":302},[10,1668,1669,313,1674,317],{},[268,1670,1672],{"href":308,"rel":1671},[272],[21,1673,312],{},[21,1675,316],{},[14,1677,1678],{"className":16,"code":320,"language":18,"meta":19,"style":19},[21,1679,1680,1684,1702,1720,1724,1734,1740,1754,1776,1806,1810,1816,1830,1852,1874,1932,1936],{"__ignoreMap":19},[24,1681,1682],{"class":26,"line":27},[24,1683,327],{"class":53},[24,1685,1686,1688,1690,1692,1694,1696,1698,1700],{"class":26,"line":57},[24,1687,31],{"class":30},[24,1689,334],{"class":34},[24,1691,337],{"class":44},[24,1693,340],{"class":34},[24,1695,343],{"class":30},[24,1697,347],{"class":346},[24,1699,312],{"class":350},[24,1701,353],{"class":346},[24,1703,1704,1706,1708,1710,1712,1714,1716,1718],{"class":26,"line":78},[24,1705,31],{"class":30},[24,1707,334],{"class":34},[24,1709,362],{"class":44},[24,1711,340],{"class":34},[24,1713,343],{"class":30},[24,1715,347],{"class":346},[24,1717,371],{"class":350},[24,1719,353],{"class":346},[24,1721,1722],{"class":26,"line":226},[24,1723,379],{"emptyLinePlaceholder":378},[24,1725,1726,1728,1730,1732],{"class":26,"line":238},[24,1727,148],{"class":30},[24,1729,386],{"class":30},[24,1731,337],{"class":389},[24,1733,392],{"class":34},[24,1735,1736,1738],{"class":26,"line":244},[24,1737,397],{"class":38},[24,1739,400],{"class":34},[24,1741,1742,1744,1746,1748,1750,1752],{"class":26,"line":403},[24,1743,406],{"class":38},[24,1745,220],{"class":34},[24,1747,411],{"class":44},[24,1749,35],{"class":34},[24,1751,416],{"class":389},[24,1753,419],{"class":34},[24,1755,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774],{"class":26,"line":422},[24,1757,425],{"class":38},[24,1759,220],{"class":34},[24,1761,411],{"class":44},[24,1763,35],{"class":34},[24,1765,434],{"class":389},[24,1767,437],{"class":34},[24,1769,440],{"class":389},[24,1771,443],{"class":34},[24,1773,447],{"class":446},[24,1775,450],{"class":34},[24,1777,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802,1804],{"class":26,"line":453},[24,1779,456],{"class":38},[24,1781,220],{"class":34},[24,1783,411],{"class":44},[24,1785,35],{"class":34},[24,1787,465],{"class":44},[24,1789,35],{"class":34},[24,1791,470],{"class":389},[24,1793,437],{"class":34},[24,1795,475],{"class":389},[24,1797,437],{"class":34},[24,1799,480],{"class":389},[24,1801,443],{"class":34},[24,1803,485],{"class":446},[24,1805,450],{"class":34},[24,1807,1808],{"class":26,"line":490},[24,1809,493],{"class":34},[24,1811,1812,1814],{"class":26,"line":496},[24,1813,499],{"class":38},[24,1815,400],{"class":34},[24,1817,1818,1820,1822,1824,1826,1828],{"class":26,"line":504},[24,1819,507],{"class":38},[24,1821,220],{"class":34},[24,1823,411],{"class":44},[24,1825,35],{"class":34},[24,1827,416],{"class":389},[24,1829,419],{"class":34},[24,1831,1832,1834,1836,1838,1840,1842,1844,1846,1848,1850],{"class":26,"line":520},[24,1833,523],{"class":38},[24,1835,220],{"class":34},[24,1837,411],{"class":44},[24,1839,35],{"class":34},[24,1841,434],{"class":389},[24,1843,437],{"class":34},[24,1845,440],{"class":389},[24,1847,443],{"class":34},[24,1849,540],{"class":446},[24,1851,450],{"class":34},[24,1853,1854,1856,1858,1860,1862,1864,1866,1868,1870,1872],{"class":26,"line":545},[24,1855,548],{"class":38},[24,1857,220],{"class":34},[24,1859,411],{"class":44},[24,1861,35],{"class":34},[24,1863,557],{"class":389},[24,1865,437],{"class":34},[24,1867,480],{"class":389},[24,1869,443],{"class":34},[24,1871,566],{"class":30},[24,1873,450],{"class":34},[24,1875,1876,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928,1930],{"class":26,"line":571},[24,1877,574],{"class":38},[24,1879,220],{"class":34},[24,1881,411],{"class":44},[24,1883,35],{"class":34},[24,1885,583],{"class":389},[24,1887,586],{"class":34},[24,1889,589],{"class":346},[24,1891,592],{"class":350},[24,1893,589],{"class":346},[24,1895,597],{"class":34},[24,1897,589],{"class":346},[24,1899,602],{"class":350},[24,1901,589],{"class":346},[24,1903,597],{"class":34},[24,1905,589],{"class":346},[24,1907,611],{"class":350},[24,1909,589],{"class":346},[24,1911,597],{"class":34},[24,1913,589],{"class":346},[24,1915,620],{"class":350},[24,1917,589],{"class":346},[24,1919,625],{"class":34},[24,1921,480],{"class":389},[24,1923,443],{"class":34},[24,1925,589],{"class":346},[24,1927,602],{"class":350},[24,1929,589],{"class":346},[24,1931,450],{"class":34},[24,1933,1934],{"class":26,"line":640},[24,1935,493],{"class":34},[24,1937,1938],{"class":26,"line":645},[24,1939,648],{"class":34},[14,1941,1942],{"className":16,"code":651,"language":18,"meta":19,"style":19},[21,1943,1944,1948,1962,1980,1984,1994,2004],{"__ignoreMap":19},[24,1945,1946],{"class":26,"line":27},[24,1947,658],{"class":53},[24,1949,1950,1952,1954,1956,1958,1960],{"class":26,"line":57},[24,1951,31],{"class":30},[24,1953,665],{"class":44},[24,1955,343],{"class":30},[24,1957,347],{"class":346},[24,1959,672],{"class":350},[24,1961,353],{"class":346},[24,1963,1964,1966,1968,1970,1972,1974,1976,1978],{"class":26,"line":78},[24,1965,31],{"class":30},[24,1967,334],{"class":34},[24,1969,683],{"class":44},[24,1971,340],{"class":34},[24,1973,343],{"class":30},[24,1975,347],{"class":346},[24,1977,692],{"class":350},[24,1979,353],{"class":346},[24,1981,1982],{"class":26,"line":226},[24,1983,379],{"emptyLinePlaceholder":378},[24,1985,1986,1988,1990,1992],{"class":26,"line":238},[24,1987,148],{"class":30},[24,1989,386],{"class":30},[24,1991,683],{"class":389},[24,1993,392],{"class":34},[24,1995,1996,1998,2000,2002],{"class":26,"line":244},[24,1997,713],{"class":38},[24,1999,716],{"class":34},[24,2001,719],{"class":389},[24,2003,722],{"class":34},[24,2005,2006],{"class":26,"line":403},[24,2007,648],{"class":34},[10,2009,729],{},[10,2011,2012,735,2014,738],{},[265,2013,734],{},[21,2015,253],{},[10,2017,2018,744],{},[265,2019,743],{},[14,2021,2022],{"className":16,"code":747,"language":18,"meta":19,"style":19},[21,2023,2024,2028,2046,2056,2066,2076,2080,2084,2102,2112,2122],{"__ignoreMap":19},[24,2025,2026],{"class":26,"line":27},[24,2027,754],{"class":53},[24,2029,2030,2032,2034,2036,2038,2040,2042,2044],{"class":26,"line":57},[24,2031,31],{"class":30},[24,2033,334],{"class":34},[24,2035,763],{"class":44},[24,2037,340],{"class":34},[24,2039,343],{"class":30},[24,2041,347],{"class":346},[24,2043,772],{"class":350},[24,2045,353],{"class":346},[24,2047,2048,2050,2052,2054],{"class":26,"line":78},[24,2049,45],{"class":44},[24,2051,35],{"class":34},[24,2053,93],{"class":44},[24,2055,785],{"class":53},[24,2057,2058,2060,2062,2064],{"class":26,"line":226},[24,2059,45],{"class":44},[24,2061,35],{"class":34},[24,2063,794],{"class":44},[24,2065,797],{"class":53},[24,2067,2068,2070,2072,2074],{"class":26,"line":238},[24,2069,45],{"class":44},[24,2071,35],{"class":34},[24,2073,170],{"class":44},[24,2075,808],{"class":53},[24,2077,2078],{"class":26,"line":244},[24,2079,379],{"emptyLinePlaceholder":378},[24,2081,2082],{"class":26,"line":403},[24,2083,817],{"class":53},[24,2085,2086,2088,2090,2092,2094,2096,2098,2100],{"class":26,"line":422},[24,2087,31],{"class":30},[24,2089,334],{"class":34},[24,2091,763],{"class":44},[24,2093,340],{"class":34},[24,2095,343],{"class":30},[24,2097,347],{"class":346},[24,2099,834],{"class":350},[24,2101,353],{"class":346},[24,2103,2104,2106,2108,2110],{"class":26,"line":453},[24,2105,45],{"class":44},[24,2107,35],{"class":34},[24,2109,170],{"class":44},[24,2111,785],{"class":53},[24,2113,2114,2116,2118,2120],{"class":26,"line":490},[24,2115,45],{"class":44},[24,2117,35],{"class":34},[24,2119,855],{"class":44},[24,2121,858],{"class":53},[24,2123,2124,2126,2128,2130],{"class":26,"line":496},[24,2125,45],{"class":44},[24,2127,35],{"class":34},[24,2129,93],{"class":44},[24,2131,869],{"class":53},[10,2133,2134,875,2136,879,2138,882],{},[265,2135,874],{},[21,2137,878],{},[21,2139,170],{},[10,2141,2142,888,2144,891,2146,894],{},[265,2143,887],{},[21,2145,102],{},[21,2147,316],{},[10,2149,2150,900,2152,903,2154,907,2156,911,2158,915,2160,918],{},[265,2151,899],{},[21,2153,834],{},[21,2155,906],{},[21,2157,910],{},[21,2159,914],{},[21,2161,906],{},[108,2163,922],{"id":921},[10,2165,925],{},[14,2167,2168],{"className":16,"code":928,"language":18,"meta":19,"style":19},[21,2169,2170,2188,2206,2210,2220,2226,2256,2260,2266,2296,2330,2334],{"__ignoreMap":19},[24,2171,2172,2174,2176,2178,2180,2182,2184,2186],{"class":26,"line":27},[24,2173,31],{"class":30},[24,2175,334],{"class":34},[24,2177,939],{"class":44},[24,2179,340],{"class":34},[24,2181,343],{"class":30},[24,2183,347],{"class":346},[24,2185,312],{"class":350},[24,2187,353],{"class":346},[24,2189,2190,2192,2194,2196,2198,2200,2202,2204],{"class":26,"line":57},[24,2191,31],{"class":30},[24,2193,956],{"class":151},[24,2195,959],{"class":30},[24,2197,962],{"class":44},[24,2199,343],{"class":30},[24,2201,347],{"class":346},[24,2203,969],{"class":350},[24,2205,353],{"class":346},[24,2207,2208],{"class":26,"line":78},[24,2209,379],{"emptyLinePlaceholder":378},[24,2211,2212,2214,2216,2218],{"class":26,"line":226},[24,2213,148],{"class":30},[24,2215,386],{"class":30},[24,2217,939],{"class":389},[24,2219,392],{"class":34},[24,2221,2222,2224],{"class":26,"line":238},[24,2223,397],{"class":38},[24,2225,400],{"class":34},[24,2227,2228,2230,2232,2234,2236,2238,2240,2242,2244,2246,2248,2250,2252,2254],{"class":26,"line":244},[24,2229,406],{"class":38},[24,2231,220],{"class":34},[24,2233,1000],{"class":44},[24,2235,35],{"class":34},[24,2237,1005],{"class":389},[24,2239,443],{"class":34},[24,2241,1000],{"class":44},[24,2243,35],{"class":34},[24,2245,434],{"class":389},[24,2247,1016],{"class":34},[24,2249,1000],{"class":44},[24,2251,35],{"class":34},[24,2253,416],{"class":389},[24,2255,1025],{"class":34},[24,2257,2258],{"class":26,"line":403},[24,2259,493],{"class":34},[24,2261,2262,2264],{"class":26,"line":422},[24,2263,499],{"class":38},[24,2265,400],{"class":34},[24,2267,2268,2270,2272,2274,2276,2278,2280,2282,2284,2286,2288,2290,2292,2294],{"class":26,"line":453},[24,2269,507],{"class":38},[24,2271,220],{"class":34},[24,2273,1000],{"class":44},[24,2275,35],{"class":34},[24,2277,1005],{"class":389},[24,2279,443],{"class":34},[24,2281,1000],{"class":44},[24,2283,35],{"class":34},[24,2285,434],{"class":389},[24,2287,1016],{"class":34},[24,2289,1000],{"class":44},[24,2291,35],{"class":34},[24,2293,416],{"class":389},[24,2295,1025],{"class":34},[24,2297,2298,2300,2302,2304,2306,2308,2310,2312,2314,2316,2318,2320,2322,2324,2326,2328],{"class":26,"line":490},[24,2299,523],{"class":38},[24,2301,220],{"class":34},[24,2303,1000],{"class":44},[24,2305,35],{"class":34},[24,2307,1005],{"class":389},[24,2309,443],{"class":34},[24,2311,1000],{"class":44},[24,2313,35],{"class":34},[24,2315,434],{"class":389},[24,2317,1016],{"class":34},[24,2319,1000],{"class":44},[24,2321,35],{"class":34},[24,2323,1094],{"class":389},[24,2325,443],{"class":34},[24,2327,540],{"class":446},[24,2329,1101],{"class":34},[24,2331,2332],{"class":26,"line":496},[24,2333,493],{"class":34},[24,2335,2336],{"class":26,"line":504},[24,2337,648],{"class":34},[10,2339,1112,2340,1116],{},[21,2341,1115],{},[108,2343,1120],{"id":1119},[14,2345,2346],{"className":16,"code":1123,"language":18,"meta":19,"style":19},[21,2347,2348,2366,2384,2402,2406,2416,2426,2432,2446,2450,2456,2470,2474],{"__ignoreMap":19},[24,2349,2350,2352,2354,2356,2358,2360,2362,2364],{"class":26,"line":27},[24,2351,31],{"class":30},[24,2353,334],{"class":34},[24,2355,337],{"class":44},[24,2357,340],{"class":34},[24,2359,343],{"class":30},[24,2361,347],{"class":346},[24,2363,312],{"class":350},[24,2365,353],{"class":346},[24,2367,2368,2370,2372,2374,2376,2378,2380,2382],{"class":26,"line":57},[24,2369,31],{"class":30},[24,2371,334],{"class":34},[24,2373,1152],{"class":44},[24,2375,340],{"class":34},[24,2377,343],{"class":30},[24,2379,347],{"class":346},[24,2381,1161],{"class":350},[24,2383,353],{"class":346},[24,2385,2386,2388,2390,2392,2394,2396,2398,2400],{"class":26,"line":78},[24,2387,31],{"class":30},[24,2389,334],{"class":34},[24,2391,362],{"class":44},[24,2393,340],{"class":34},[24,2395,343],{"class":30},[24,2397,347],{"class":346},[24,2399,371],{"class":350},[24,2401,353],{"class":346},[24,2403,2404],{"class":26,"line":226},[24,2405,379],{"emptyLinePlaceholder":378},[24,2407,2408,2410,2412,2414],{"class":26,"line":238},[24,2409,148],{"class":30},[24,2411,386],{"class":30},[24,2413,337],{"class":389},[24,2415,392],{"class":34},[24,2417,2418,2420,2422,2424],{"class":26,"line":244},[24,2419,1200],{"class":38},[24,2421,716],{"class":34},[24,2423,1205],{"class":44},[24,2425,1208],{"class":34},[24,2427,2428,2430],{"class":26,"line":403},[24,2429,397],{"class":38},[24,2431,400],{"class":34},[24,2433,2434,2436,2438,2440,2442,2444],{"class":26,"line":422},[24,2435,406],{"class":38},[24,2437,220],{"class":34},[24,2439,411],{"class":44},[24,2441,35],{"class":34},[24,2443,416],{"class":389},[24,2445,419],{"class":34},[24,2447,2448],{"class":26,"line":453},[24,2449,493],{"class":34},[24,2451,2452,2454],{"class":26,"line":490},[24,2453,499],{"class":38},[24,2455,400],{"class":34},[24,2457,2458,2460,2462,2464,2466,2468],{"class":26,"line":496},[24,2459,507],{"class":38},[24,2461,220],{"class":34},[24,2463,411],{"class":44},[24,2465,35],{"class":34},[24,2467,416],{"class":389},[24,2469,419],{"class":34},[24,2471,2472],{"class":26,"line":504},[24,2473,493],{"class":34},[24,2475,2476],{"class":26,"line":520},[24,2477,648],{"class":34},[10,2479,1263,2480,597,2482,597,2484,1272],{},[21,2481,1205],{},[21,2483,1268],{},[21,2485,1271],{},[108,2487,1276],{"id":1275},[10,2489,2490,1282],{},[265,2491,1281],{},[10,2493,2494,1291,2498,1295,2500,1299],{},[265,2495,1287,2496,1290],{},[21,2497,295],{},[21,2499,1294],{},[21,2501,1298],{},[10,2503,2504,1305,2506,597,2508,597,2510,597,2512,597,2514,597,2516,1323],{},[265,2505,1304],{},[21,2507,1308],{},[21,2509,1311],{},[21,2511,1314],{},[21,2513,1317],{},[21,2515,878],{},[21,2517,1322],{},[108,2519,1327],{"id":1326},[14,2521,2522],{"className":1330,"code":1331,"language":1332,"meta":19,"style":19},[21,2523,2524,2528,2536,2540,2544,2552,2556,2560],{"__ignoreMap":19},[24,2525,2526],{"class":26,"line":27},[24,2527,1339],{"class":53},[24,2529,2530,2532,2534],{"class":26,"line":57},[24,2531,1344],{"class":389},[24,2533,1347],{"class":350},[24,2535,1350],{"class":350},[24,2537,2538],{"class":26,"line":78},[24,2539,379],{"emptyLinePlaceholder":378},[24,2541,2542],{"class":26,"line":226},[24,2543,1359],{"class":53},[24,2545,2546,2548,2550],{"class":26,"line":238},[24,2547,1344],{"class":389},[24,2549,1347],{"class":350},[24,2551,1368],{"class":350},[24,2553,2554],{"class":26,"line":244},[24,2555,379],{"emptyLinePlaceholder":378},[24,2557,2558],{"class":26,"line":403},[24,2559,1377],{"class":53},[24,2561,2562,2564,2566],{"class":26,"line":422},[24,2563,1344],{"class":389},[24,2565,1347],{"class":350},[24,2567,1386],{"class":350},[10,2569,2570,1392,2572,1396],{},[21,2571,1391],{},[21,2573,1395],{},[108,2575,1400],{"id":1399},[1402,2577,2578,2583],{},[1405,2579,1407,2580],{},[268,2581,1411],{"href":308,"rel":2582},[272],[1405,2584,1414,2585],{},[268,2586,1419],{"href":1417,"rel":2587},[272],[14,2589,2590],{"className":1330,"code":1422,"language":1332,"meta":19,"style":19},[21,2591,2592],{"__ignoreMap":19},[24,2593,2594,2596,2598,2600],{"class":26,"line":27},[24,2595,1429],{"class":389},[24,2597,1432],{"class":350},[24,2599,1435],{"class":350},[24,2601,1438],{"class":350},[10,2603,1441],{},[1443,2605,1445],{},{"title":19,"searchDepth":57,"depth":57,"links":2607},[2608,2609,2610,2611,2612,2613,2614,2615,2616,2617,2618],{"id":110,"depth":57,"text":111},{"id":125,"depth":57,"text":126},{"id":179,"depth":57,"text":180},{"id":186,"depth":57,"text":187},{"id":257,"depth":57,"text":258},{"id":302,"depth":57,"text":303},{"id":921,"depth":57,"text":922},{"id":1119,"depth":57,"text":1120},{"id":1275,"depth":57,"text":1276},{"id":1326,"depth":57,"text":1327},{"id":1399,"depth":57,"text":1400},{},{"title":5,"description":1461},{"id":2622,"title":2623,"body":2624,"category":3161,"date":3162,"description":3163,"draft":1462,"extension":1463,"meta":3164,"navigation":378,"path":3165,"seo":3166,"sitemap":378,"stem":3167,"__hash__":3168},"writings\u002Fwritings\u002Fstop-fighting-localhost-stable-dev-urls-with-cloudflared.md","Stop Fighting localhost: Stable Dev URLs with Cloudflared",{"type":7,"value":2625,"toc":3148},[2626,2629,2647,2656,2660,2666,2669,2684,2687,2707,2710,2714,2717,2720,2734,2743,2746,2750,2753,2762,2765,2785,2794,2799,2807,2810,2821,2824,2838,2842,2845,2854,2868,2871,2875,2878,2894,2900,2903,2911,2914,2923,2927,2935,2938,3041,3044,3067,3070,3074,3077,3080,3094,3097,3101,3104,3107,3118,3121,3124,3128,3131,3134,3142,3145],[10,2627,2628],{},"Local development is easy until it isn't.",[10,2630,2631,2632,2637,2638,597,2643,2646],{},"The moment your project touches ",[268,2633,2636],{"href":2634,"rel":2635},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWebhook",[272],"webhooks",", OAuth callbacks, or third-party platforms like ",[268,2639,2642],{"href":2640,"rel":2641},"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fmessaging-api\u002Foverview\u002F",[272],"LINE Official Account",[21,2644,2645],{},"http:\u002F\u002Flocalhost"," becomes useless. No HTTPS. No public access. No stability. Just a wall you keep running into while trying to ship features.",[10,2648,2649,2650,2655],{},"After fighting this problem across multiple projects, I stopped treating it as a temporary inconvenience and fixed it properly. ",[268,2651,2654],{"href":2652,"rel":2653},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fconnections\u002Fconnect-networks\u002F",[272],"Cloudflared Tunnel"," ended up being the cleanest solution.",[108,2657,2659],{"id":2658},"the-localhost-wall","The Localhost Wall",[10,2661,2662,2663,35],{},"Many platforms simply refuse to talk to ",[21,2664,2665],{},"localhost",[10,2667,2668],{},"LINE Messaging API, payment gateways, OAuth providers, and most webhook-based services expect one thing: a public HTTPS URL. They don't care that you're \"just developing.\" From their perspective, your server either exists on the internet or it doesn't.",[10,2670,2671,2672,2677,2678,2683],{},"LINE, for example, explicitly requires webhook endpoints to be public and HTTPS-only (",[268,2673,2676],{"href":2674,"rel":2675},"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fmessaging-api\u002F",[272],"LINE Messaging API docs","). OAuth providers follow similarly strict rules around redirect URIs (",[268,2679,2682],{"href":2680,"rel":2681},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc6749#section-3.1.2",[272],"OAuth 2.0 RFC 6749",").",[10,2685,2686],{},"The usual workflow looks like this:",[1402,2688,2689,2692,2695,2698,2701,2704],{},[1405,2690,2691],{},"Start a local server",[1405,2693,2694],{},"Expose it using a tunnel",[1405,2696,2697],{},"Get a random public URL",[1405,2699,2700],{},"Update webhook or callback settings",[1405,2702,2703],{},"Restart the server",[1405,2705,2706],{},"Repeat everything because the URL changed",[10,2708,2709],{},"It works, but it's fragile. The moment you restart your machine or your tunnel drops, everything breaks again.",[108,2711,2713],{"id":2712},"why-random-urls-arent-enough","Why Random URLs Aren't Enough",[10,2715,2716],{},"Tools that expose a random public URL without authentication are convenient, but they don't scale beyond quick demos.",[10,2718,2719],{},"The problems show up fast:",[1402,2721,2722,2725,2728,2731],{},[1405,2723,2724],{},"Webhook URLs change every restart",[1405,2726,2727],{},"OAuth redirect URIs need constant updates",[1405,2729,2730],{},"Hard to share a stable endpoint with teammates",[1405,2732,2733],{},"Impossible to treat dev like a real environment",[10,2735,2736,2737,2742],{},"Some tools offer reserved domains as a paid feature (",[268,2738,2741],{"href":2739,"rel":2740},"https:\u002F\u002Fngrok.com\u002Fdocs\u002Fguides\u002F",[272],"ngrok guide","), which helps, but the underlying model still feels temporary rather than infrastructural.",[10,2744,2745],{},"For webhook-driven systems, instability is the real enemy. You want your dev environment to behave like production, just smaller and safer.",[108,2747,2749],{"id":2748},"what-cloudflared-tunnel-actually-solves","What Cloudflared Tunnel Actually Solves",[10,2751,2752],{},"Cloudflared Tunnel flips the model.",[10,2754,2755,2756,2761],{},"Instead of opening a port or exposing your machine directly, it creates an outbound tunnel from your local server to Cloudflare's network. Cloudflare handles HTTPS, DNS, and routing (",[268,2757,2760],{"href":2758,"rel":2759},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fdns\u002F",[272],"Cloudflare DNS","). Your machine never accepts inbound traffic.",[10,2763,2764],{},"This gives you:",[1402,2766,2767,2776,2779,2782],{},[1405,2768,2769,2770,2775],{},"HTTPS by default (",[268,2771,2774],{"href":2772,"rel":2773},"https:\u002F\u002Fwww.cloudflare.com\u002Flearning\u002Fssl\u002Fwhat-is-https\u002F",[272],"why HTTPS matters",")",[1405,2777,2778],{},"No public IP required",[1405,2780,2781],{},"No firewall or NAT configuration",[1405,2783,2784],{},"Production-grade networking for local development",[10,2786,2787,2788,2793],{},"Cloudflared supports two distinct modes (",[268,2789,2792],{"href":2790,"rel":2791},"https:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fcloudflared",[272],"cloudflared on GitHub","), and the difference matters.",[2795,2796,2798],"h3",{"id":2797},"temporary-tunnels-no-login-no-domain","Temporary Tunnels (No Login, No Domain)",[10,2800,2801,2802,2683],{},"You can run Cloudflared without logging in or owning a domain. It gives you a random HTTPS URL that forwards traffic to your local server (",[268,2803,2806],{"href":2804,"rel":2805},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fnetworks\u002Fconnectors\u002Fcloudflare-tunnel\u002Fconfigure-tunnels\u002Ftunnel-availability\u002Fdeploy-replicas\u002F#locally-managed-tunnels",[272],"run a local tunnel",[10,2808,2809],{},"This is fine for:",[1402,2811,2812,2815,2818],{},[1405,2813,2814],{},"Quick testing",[1405,2816,2817],{},"Demos",[1405,2819,2820],{},"Debugging something once",[10,2822,2823],{},"It's not fine for:",[1402,2825,2826,2829,2832,2835],{},[1405,2827,2828],{},"Webhooks",[1405,2830,2831],{},"OAuth",[1405,2833,2834],{},"Long-running development",[1405,2836,2837],{},"Anything you need to configure once and forget",[2795,2839,2841],{"id":2840},"stable-tunnels-login-custom-domain","Stable Tunnels (Login + Custom Domain)",[10,2843,2844],{},"This is where Cloudflared becomes genuinely useful.",[10,2846,2847,2848,2853],{},"When you log in and attach a domain (",[268,2849,2852],{"href":2850,"rel":2851},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fconnections\u002Fconnect-networks\u002Fget-started\u002Fcreate-local-tunnel\u002F",[272],"create a tunnel","), you get:",[1402,2855,2856,2859,2862,2865],{},[1405,2857,2858],{},"Stable subdomains",[1405,2860,2861],{},"Persistent URLs",[1405,2863,2864],{},"Multiple services behind one tunnel",[1405,2866,2867],{},"Zero reconfiguration after restarts",[10,2869,2870],{},"Your local machine starts behaving like a real environment.",[108,2872,2874],{"id":2873},"a-real-use-case-line-oa-chatbot-development","A Real Use Case: LINE OA Chatbot Development",[10,2876,2877],{},"LINE Official Account webhooks are strict. They must be:",[1402,2879,2880,2883,2886],{},[1405,2881,2882],{},"Public",[1405,2884,2885],{},"HTTPS",[1405,2887,2888,2889,2775],{},"Always reachable (",[268,2890,2893],{"href":2891,"rel":2892},"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fmessaging-api\u002Freceiving-messages\u002F",[272],"receiving messages",[10,2895,2896,2897,35],{},"They do not support ",[21,2898,2899],{},"http:\u002F\u002Flocalhost:\u003Cport>",[10,2901,2902],{},"With Cloudflared, the flow becomes simple:",[14,2904,2909],{"className":2905,"code":2907,"language":2908},[2906],"language-text","LINE Platform\n    ↓\nhttps:\u002F\u002Fapi-dev.fasu.dev\u002Fwebhook\n    ↓\nCloudflared Tunnel\n    ↓\nhttp:\u002F\u002Flocalhost:8787\n","text",[21,2910,2907],{"__ignoreMap":19},[10,2912,2913],{},"Your chatbot runs locally. LINE talks to a real HTTPS URL. Nothing breaks when you restart your server.",[10,2915,2916,2917,2922],{},"This setup pairs especially well with ",[268,2918,2921],{"href":2919,"rel":2920},"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fliff\u002F",[272],"LINE LIFF",", where a frontend LIFF app and backend webhook often need to evolve together.",[108,2924,2926],{"id":2925},"one-tunnel-multiple-services","One Tunnel, Multiple Services",[10,2928,2929,2930,2683],{},"A single Cloudflared tunnel can expose multiple local services under different subdomains using ingress rules (",[268,2931,2934],{"href":2932,"rel":2933},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fconnections\u002Fconnect-networks\u002Fconfigure-tunnels\u002Flocal-management\u002Fconfiguration-file\u002F",[272],"ingress configuration reference",[10,2936,2937],{},"Example configuration:",[14,2939,2943],{"className":2940,"code":2941,"language":2942,"meta":19,"style":19},"language-yaml shiki shiki-themes vitesse-light vitesse-dark","tunnel: fasu-dev\ncredentials-file: ~\u002F.cloudflared\u002Fcredentials.json\n\ningress:\n  - hostname: dev.fasu.dev\n    service: http:\u002F\u002Flocalhost:3000\n\n  - hostname: api-dev.fasu.dev\n    service: http:\u002F\u002Flocalhost:8787\n\n  - service: http_status:404\n","yaml",[21,2944,2945,2956,2966,2970,2978,2991,3001,3005,3016,3025,3029],{"__ignoreMap":19},[24,2946,2947,2950,2953],{"class":26,"line":27},[24,2948,2949],{"class":38},"tunnel",[24,2951,2952],{"class":34},":",[24,2954,2955],{"class":350}," fasu-dev\n",[24,2957,2958,2961,2963],{"class":26,"line":57},[24,2959,2960],{"class":38},"credentials-file",[24,2962,2952],{"class":34},[24,2964,2965],{"class":350}," ~\u002F.cloudflared\u002Fcredentials.json\n",[24,2967,2968],{"class":26,"line":78},[24,2969,379],{"emptyLinePlaceholder":378},[24,2971,2972,2975],{"class":26,"line":226},[24,2973,2974],{"class":38},"ingress",[24,2976,2977],{"class":34},":\n",[24,2979,2980,2983,2986,2988],{"class":26,"line":238},[24,2981,2982],{"class":34},"  -",[24,2984,2985],{"class":38}," hostname",[24,2987,2952],{"class":34},[24,2989,2990],{"class":350}," dev.fasu.dev\n",[24,2992,2993,2996,2998],{"class":26,"line":244},[24,2994,2995],{"class":38},"    service",[24,2997,2952],{"class":34},[24,2999,3000],{"class":350}," http:\u002F\u002Flocalhost:3000\n",[24,3002,3003],{"class":26,"line":403},[24,3004,379],{"emptyLinePlaceholder":378},[24,3006,3007,3009,3011,3013],{"class":26,"line":422},[24,3008,2982],{"class":34},[24,3010,2985],{"class":38},[24,3012,2952],{"class":34},[24,3014,3015],{"class":350}," api-dev.fasu.dev\n",[24,3017,3018,3020,3022],{"class":26,"line":453},[24,3019,2995],{"class":38},[24,3021,2952],{"class":34},[24,3023,3024],{"class":350}," http:\u002F\u002Flocalhost:8787\n",[24,3026,3027],{"class":26,"line":490},[24,3028,379],{"emptyLinePlaceholder":378},[24,3030,3031,3033,3036,3038],{"class":26,"line":496},[24,3032,2982],{"class":34},[24,3034,3035],{"class":38}," service",[24,3037,2952],{"class":34},[24,3039,3040],{"class":350}," http_status:404\n",[10,3042,3043],{},"Now you have:",[1402,3045,3046,3052,3058,3061,3064],{},[1405,3047,3048,3051],{},[21,3049,3050],{},"dev.fasu.dev"," for your frontend",[1405,3053,3054,3057],{},[21,3055,3056],{},"api-dev.fasu.dev"," for your backend",[1405,3059,3060],{},"Both running locally",[1405,3062,3063],{},"Both HTTPS",[1405,3065,3066],{},"Both stable",[10,3068,3069],{},"This mirrors how real environments are structured, instead of treating development as a special case.",[108,3071,3073],{"id":3072},"why-this-beats-the-usual-alternatives","Why This Beats the Usual Alternatives",[10,3075,3076],{},"This isn't about declaring winners. It's about choosing tools that match the problem.",[10,3078,3079],{},"Cloudflared tunnels feel less like a workaround and more like real infrastructure:",[1402,3081,3082,3085,3088,3091],{},[1405,3083,3084],{},"URLs don't change",[1405,3086,3087],{},"Domains look like production",[1405,3089,3090],{},"Webhooks don't need reconfiguration",[1405,3092,3093],{},"Dev, staging, and preview environments can share the same pattern",[10,3095,3096],{},"Once set up, it disappears into the background. That's exactly what infrastructure should do.",[108,3098,3100],{"id":3099},"when-cloudflared-is-overkill","When Cloudflared Is Overkill",[10,3102,3103],{},"Not every project needs this.",[10,3105,3106],{},"If you're:",[1402,3108,3109,3112,3115],{},[1405,3110,3111],{},"Sharing a quick demo",[1405,3113,3114],{},"Testing something once",[1405,3116,3117],{},"Debugging a one-off webhook",[10,3119,3120],{},"A temporary tunnel is fine.",[10,3122,3123],{},"But if your workflow depends on webhooks, callbacks, or third-party integrations, stability is not optional.",[108,3125,3127],{"id":3126},"final-thoughts","Final Thoughts",[10,3129,3130],{},"The mistake I made early on was treating webhooks as a production-only concern.",[10,3132,3133],{},"They aren't.",[10,3135,3136,3137,35],{},"If your dev environment behaves differently from production, you'll spend your time fighting tools instead of building features. This idea maps closely to ",[268,3138,3141],{"href":3139,"rel":3140},"https:\u002F\u002F12factor.net\u002Fdev-prod-parity",[272],"dev–prod parity",[10,3143,3144],{},"Once your local URLs stop changing, everything else gets easier.",[1443,3146,3147],{},"html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":19,"searchDepth":57,"depth":57,"links":3149},[3150,3151,3152,3156,3157,3158,3159,3160],{"id":2658,"depth":57,"text":2659},{"id":2712,"depth":57,"text":2713},{"id":2748,"depth":57,"text":2749,"children":3153},[3154,3155],{"id":2797,"depth":78,"text":2798},{"id":2840,"depth":78,"text":2841},{"id":2873,"depth":57,"text":2874},{"id":2925,"depth":57,"text":2926},{"id":3072,"depth":57,"text":3073},{"id":3099,"depth":57,"text":3100},{"id":3126,"depth":57,"text":3127},"tutorial","2026-02-04","Stable HTTPS URLs for local dev with Cloudflared Tunnel — no more broken webhooks after every restart.",{},"\u002Fwritings\u002Fstop-fighting-localhost-stable-dev-urls-with-cloudflared",{"title":2623,"description":3163},"writings\u002Fstop-fighting-localhost-stable-dev-urls-with-cloudflared","G30c7TXNOmxeHVPUt7QMZsSla0jy6X56D_QY4BSZSg8",{"id":3170,"title":3171,"body":3172,"category":3161,"date":4017,"description":4018,"draft":1462,"extension":1463,"meta":4019,"navigation":378,"path":4020,"seo":4021,"sitemap":378,"stem":4022,"__hash__":4023},"writings\u002Fwritings\u002Fgit-from-zero-to-pull-request-a-practical-guide.md","Git from Zero to Pull Request: A Practical Guide",{"type":7,"value":3173,"toc":4006},[3174,3177,3181,3184,3187,3190,3194,3197,3213,3216,3235,3238,3254,3263,3266,3312,3315,3327,3331,3334,3340,3367,3373,3388,3395,3410,3414,3417,3429,3432,3435,3476,3479,3501,3504,3507,3510,3535,3542,3569,3575,3579,3582,3585,3620,3634,3641,3644,3658,3661,3665,3672,3675,3702,3705,3720,3723,3739,3758,3764,3777,3781,3787,3790,3807,3810,3813,3816,3820,3823,3832,3838,3841,3873,3890,3919,3922,3937,3940,3944,3959,3967,3982,3990,4000,4003],[10,3175,3176],{},"You will use Git every day as a developer. Most tutorials either hand you a cheat sheet with no context or walk you through theory you'll never need. This is neither. It's the practical knowledge I wish someone had given me on day one — what Git does, how to use it, and why the commands work the way they do.",[108,3178,3180],{"id":3179},"what-git-is-and-why-it-matters","What Git Is and Why It Matters",[10,3182,3183],{},"Git is a version control system that tracks changes to your files over time. That's it. Every change you save becomes part of a history you can browse, search, and undo.",[10,3185,3186],{},"Teams use Git because it solves three problems at once: it keeps a complete history of every change, it lets multiple people work on the same codebase without overwriting each other, and it makes rollbacks trivial when something breaks.",[10,3188,3189],{},"One distinction worth making early: Git is the tool that runs on your machine. GitHub is a hosting platform where you store Git repositories online. You can use Git without GitHub — but most teams use both.",[108,3191,3193],{"id":3192},"installation-and-setup","Installation and Setup",[10,3195,3196],{},"On macOS with Homebrew:",[14,3198,3200],{"className":1330,"code":3199,"language":1332,"meta":19,"style":19},"brew install git\n",[21,3201,3202],{"__ignoreMap":19},[24,3203,3204,3207,3210],{"class":26,"line":27},[24,3205,3206],{"class":389},"brew",[24,3208,3209],{"class":350}," install",[24,3211,3212],{"class":350}," git\n",[10,3214,3215],{},"On Arch Linux:",[14,3217,3219],{"className":1330,"code":3218,"language":1332,"meta":19,"style":19},"sudo pacman -S git\n",[21,3220,3221],{"__ignoreMap":19},[24,3222,3223,3226,3229,3233],{"class":26,"line":27},[24,3224,3225],{"class":389},"sudo",[24,3227,3228],{"class":350}," pacman",[24,3230,3232],{"class":3231},"sfsYZ"," -S",[24,3234,3212],{"class":350},[10,3236,3237],{},"On Ubuntu or Debian:",[14,3239,3241],{"className":1330,"code":3240,"language":1332,"meta":19,"style":19},"sudo apt install git\n",[21,3242,3243],{"__ignoreMap":19},[24,3244,3245,3247,3250,3252],{"class":26,"line":27},[24,3246,3225],{"class":389},[24,3248,3249],{"class":350}," apt",[24,3251,3209],{"class":350},[24,3253,3212],{"class":350},[10,3255,3256,3257,3262],{},"On Windows, download the installer from ",[268,3258,3261],{"href":3259,"rel":3260},"https:\u002F\u002Fgit-scm.com",[272],"git-scm.com"," and follow the prompts. The defaults are fine.",[10,3264,3265],{},"After installing, configure your identity. Git attaches this to every commit you make:",[14,3267,3269],{"className":1330,"code":3268,"language":1332,"meta":19,"style":19},"git config --global user.name \"Your Name\"\ngit config --global user.email \"your@email.com\"\n",[21,3270,3271,3294],{"__ignoreMap":19},[24,3272,3273,3276,3279,3282,3285,3288,3291],{"class":26,"line":27},[24,3274,3275],{"class":389},"git",[24,3277,3278],{"class":350}," config",[24,3280,3281],{"class":3231}," --global",[24,3283,3284],{"class":350}," user.name",[24,3286,3287],{"class":346}," \"",[24,3289,3290],{"class":350},"Your Name",[24,3292,3293],{"class":346},"\"\n",[24,3295,3296,3298,3300,3302,3305,3307,3310],{"class":26,"line":57},[24,3297,3275],{"class":389},[24,3299,3278],{"class":350},[24,3301,3281],{"class":3231},[24,3303,3304],{"class":350}," user.email",[24,3306,3287],{"class":346},[24,3308,3309],{"class":350},"your@email.com",[24,3311,3293],{"class":346},[10,3313,3314],{},"Verify everything works:",[14,3316,3318],{"className":1330,"code":3317,"language":1332,"meta":19,"style":19},"git --version\n",[21,3319,3320],{"__ignoreMap":19},[24,3321,3322,3324],{"class":26,"line":27},[24,3323,3275],{"class":389},[24,3325,3326],{"class":3231}," --version\n",[108,3328,3330],{"id":3329},"repositories","Repositories",[10,3332,3333],{},"A repository is a project tracked by Git. You create one in two ways.",[10,3335,3336,3339],{},[21,3337,3338],{},"git init"," creates a new repository in the current directory:",[14,3341,3343],{"className":1330,"code":3342,"language":1332,"meta":19,"style":19},"mkdir my-project\ncd my-project\ngit init\n",[21,3344,3345,3353,3360],{"__ignoreMap":19},[24,3346,3347,3350],{"class":26,"line":27},[24,3348,3349],{"class":389},"mkdir",[24,3351,3352],{"class":350}," my-project\n",[24,3354,3355,3358],{"class":26,"line":57},[24,3356,3357],{"class":38},"cd",[24,3359,3352],{"class":350},[24,3361,3362,3364],{"class":26,"line":78},[24,3363,3275],{"class":389},[24,3365,3366],{"class":350}," init\n",[10,3368,3369,3372],{},[21,3370,3371],{},"git clone"," copies an existing repository from a remote URL:",[14,3374,3376],{"className":1330,"code":3375,"language":1332,"meta":19,"style":19},"git clone https:\u002F\u002Fgithub.com\u002Fuser\u002Frepo.git\n",[21,3377,3378],{"__ignoreMap":19},[24,3379,3380,3382,3385],{"class":26,"line":27},[24,3381,3275],{"class":389},[24,3383,3384],{"class":350}," clone",[24,3386,3387],{"class":350}," https:\u002F\u002Fgithub.com\u002Fuser\u002Frepo.git\n",[10,3389,3390,3391,3394],{},"Both create a hidden ",[21,3392,3393],{},".git\u002F"," directory inside your project. That directory is the repository — it contains the entire history of your project. Delete it and you lose the history. The rest of your project files are just the working directory.",[10,3396,3397,3398,3401,3402,3405,3406,3409],{},"Git tracks files through three states. The ",[265,3399,3400],{},"working directory"," is where you edit files. The ",[265,3403,3404],{},"staging area"," is where you prepare changes for a commit. The ",[265,3407,3408],{},"repository"," is where committed snapshots are stored permanently. Understanding this flow — edit, stage, commit — is the mental model that makes every other Git command make sense.",[108,3411,3413],{"id":3412},"tracking-files-and-making-commits","Tracking Files and Making Commits",[10,3415,3416],{},"Check what Git sees:",[14,3418,3420],{"className":1330,"code":3419,"language":1332,"meta":19,"style":19},"git status\n",[21,3421,3422],{"__ignoreMap":19},[24,3423,3424,3426],{"class":26,"line":27},[24,3425,3275],{"class":389},[24,3427,3428],{"class":350}," status\n",[10,3430,3431],{},"This shows which files are modified, which are staged, and which are untracked. Run it often — it's your orientation command.",[10,3433,3434],{},"Stage files for a commit:",[14,3436,3438],{"className":1330,"code":3437,"language":1332,"meta":19,"style":19},"git add index.html           # stage one file\ngit add src\u002F                 # stage an entire directory\ngit add .                    # stage everything\n",[21,3439,3440,3452,3464],{"__ignoreMap":19},[24,3441,3442,3444,3446,3449],{"class":26,"line":27},[24,3443,3275],{"class":389},[24,3445,1432],{"class":350},[24,3447,3448],{"class":350}," index.html",[24,3450,3451],{"class":53},"           # stage one file\n",[24,3453,3454,3456,3458,3461],{"class":26,"line":57},[24,3455,3275],{"class":389},[24,3457,1432],{"class":350},[24,3459,3460],{"class":350}," src\u002F",[24,3462,3463],{"class":53},"                 # stage an entire directory\n",[24,3465,3466,3468,3470,3473],{"class":26,"line":78},[24,3467,3275],{"class":389},[24,3469,1432],{"class":350},[24,3471,3472],{"class":350}," .",[24,3474,3475],{"class":53},"                    # stage everything\n",[10,3477,3478],{},"Create a commit — a snapshot of your staged changes:",[14,3480,3482],{"className":1330,"code":3481,"language":1332,"meta":19,"style":19},"git commit -m \"Add homepage layout\"\n",[21,3483,3484],{"__ignoreMap":19},[24,3485,3486,3488,3491,3494,3496,3499],{"class":26,"line":27},[24,3487,3275],{"class":389},[24,3489,3490],{"class":350}," commit",[24,3492,3493],{"class":3231}," -m",[24,3495,3287],{"class":346},[24,3497,3498],{"class":350},"Add homepage layout",[24,3500,3293],{"class":346},[10,3502,3503],{},"A commit is a snapshot, not a diff. Git stores the complete state of your staged files at that point in time. Diffs are computed later by comparing snapshots.",[10,3505,3506],{},"Good commit messages use imperative mood and a short subject line: \"Add login form,\" \"Fix null check in user service,\" \"Remove deprecated API endpoint.\" Describe what the commit does, not what you did. Keep the subject under 50 characters. If you need more detail, leave a blank line and write a body.",[10,3508,3509],{},"View your commit history:",[14,3511,3513],{"className":1330,"code":3512,"language":1332,"meta":19,"style":19},"git log\ngit log --oneline           # compact view\n",[21,3514,3515,3522],{"__ignoreMap":19},[24,3516,3517,3519],{"class":26,"line":27},[24,3518,3275],{"class":389},[24,3520,3521],{"class":350}," log\n",[24,3523,3524,3526,3529,3532],{"class":26,"line":57},[24,3525,3275],{"class":389},[24,3527,3528],{"class":350}," log",[24,3530,3531],{"class":3231}," --oneline",[24,3533,3534],{"class":53},"           # compact view\n",[10,3536,3537,3538,3541],{},"Some files should never be tracked — build artifacts, environment variables, dependency directories. Create a ",[21,3539,3540],{},".gitignore"," file in your project root:",[14,3543,3547],{"className":3544,"code":3545,"language":3546,"meta":19,"style":19},"language-gitignore shiki shiki-themes vitesse-light vitesse-dark","node_modules\u002F\n.env\ndist\u002F\n*.log\n","gitignore",[21,3548,3549,3554,3559,3564],{"__ignoreMap":19},[24,3550,3551],{"class":26,"line":27},[24,3552,3553],{},"node_modules\u002F\n",[24,3555,3556],{"class":26,"line":57},[24,3557,3558],{},".env\n",[24,3560,3561],{"class":26,"line":78},[24,3562,3563],{},"dist\u002F\n",[24,3565,3566],{"class":26,"line":226},[24,3567,3568],{},"*.log\n",[10,3570,3571,3572,3574],{},"Git will ignore anything matching these patterns. Add ",[21,3573,3540],{}," early. Removing a file from Git after it's been committed is more work than preventing it from being tracked in the first place.",[108,3576,3578],{"id":3577},"using-github","Using GitHub",[10,3580,3581],{},"Create a repository on GitHub through the web interface. Don't initialize it with a README if you already have local commits — that creates a conflict.",[10,3583,3584],{},"Link your local repository to the remote:",[14,3586,3588],{"className":1330,"code":3587,"language":1332,"meta":19,"style":19},"git remote add origin https:\u002F\u002Fgithub.com\u002Fyour-username\u002Fyour-repo.git\ngit push -u origin main\n",[21,3589,3590,3605],{"__ignoreMap":19},[24,3591,3592,3594,3597,3599,3602],{"class":26,"line":27},[24,3593,3275],{"class":389},[24,3595,3596],{"class":350}," remote",[24,3598,1432],{"class":350},[24,3600,3601],{"class":350}," origin",[24,3603,3604],{"class":350}," https:\u002F\u002Fgithub.com\u002Fyour-username\u002Fyour-repo.git\n",[24,3606,3607,3609,3612,3615,3617],{"class":26,"line":57},[24,3608,3275],{"class":389},[24,3610,3611],{"class":350}," push",[24,3613,3614],{"class":3231}," -u",[24,3616,3601],{"class":350},[24,3618,3619],{"class":350}," main\n",[10,3621,3622,3623,3626,3627,3630,3631,35],{},"The ",[21,3624,3625],{},"-u"," flag sets ",[21,3628,3629],{},"origin main"," as the default upstream, so future pushes only need ",[21,3632,3633],{},"git push",[10,3635,3636,3637,3640],{},"GitHub supports two authentication methods: HTTPS and SSH. HTTPS prompts for credentials (use a personal access token, not your password). SSH uses a key pair and never prompts after setup. SSH is less friction day-to-day — set it up once with ",[21,3638,3639],{},"ssh-keygen"," and add the public key to your GitHub settings.",[10,3642,3643],{},"Clone someone else's repository to get a local copy:",[14,3645,3647],{"className":1330,"code":3646,"language":1332,"meta":19,"style":19},"git clone https:\u002F\u002Fgithub.com\u002Fother-user\u002Ftheir-repo.git\n",[21,3648,3649],{"__ignoreMap":19},[24,3650,3651,3653,3655],{"class":26,"line":27},[24,3652,3275],{"class":389},[24,3654,3384],{"class":350},[24,3656,3657],{"class":350}," https:\u002F\u002Fgithub.com\u002Fother-user\u002Ftheir-repo.git\n",[10,3659,3660],{},"Beyond code hosting, GitHub adds a social layer: README files describe the project, Issues track bugs and feature requests, and Stars bookmark repositories you find useful. These aren't Git features — they're GitHub features.",[108,3662,3664],{"id":3663},"branching","Branching",[10,3666,3667,3668,3671],{},"A branch is an independent line of development. The default branch is ",[21,3669,3670],{},"main",". Every other branch diverges from it and can be merged back later.",[10,3673,3674],{},"Create and list branches:",[14,3676,3678],{"className":1330,"code":3677,"language":1332,"meta":19,"style":19},"git branch                  # list branches\ngit branch feature\u002Flogin    # create a branch\n",[21,3679,3680,3690],{"__ignoreMap":19},[24,3681,3682,3684,3687],{"class":26,"line":27},[24,3683,3275],{"class":389},[24,3685,3686],{"class":350}," branch",[24,3688,3689],{"class":53},"                  # list branches\n",[24,3691,3692,3694,3696,3699],{"class":26,"line":57},[24,3693,3275],{"class":389},[24,3695,3686],{"class":350},[24,3697,3698],{"class":350}," feature\u002Flogin",[24,3700,3701],{"class":53},"    # create a branch\n",[10,3703,3704],{},"Switch to a branch:",[14,3706,3708],{"className":1330,"code":3707,"language":1332,"meta":19,"style":19},"git switch feature\u002Flogin\n",[21,3709,3710],{"__ignoreMap":19},[24,3711,3712,3714,3717],{"class":26,"line":27},[24,3713,3275],{"class":389},[24,3715,3716],{"class":350}," switch",[24,3718,3719],{"class":350}," feature\u002Flogin\n",[10,3721,3722],{},"Create and switch in one step:",[14,3724,3726],{"className":1330,"code":3725,"language":1332,"meta":19,"style":19},"git switch -c feature\u002Flogin\n",[21,3727,3728],{"__ignoreMap":19},[24,3729,3730,3732,3734,3737],{"class":26,"line":27},[24,3731,3275],{"class":389},[24,3733,3716],{"class":350},[24,3735,3736],{"class":3231}," -c",[24,3738,3719],{"class":350},[10,3740,3741,3742,3745,3746,3749,3750,3753,3754,3757],{},"I use ",[21,3743,3744],{},"git switch"," instead of ",[21,3747,3748],{},"git checkout"," for branch operations. ",[21,3751,3752],{},"switch"," was introduced specifically for this purpose and is less ambiguous — ",[21,3755,3756],{},"checkout"," does too many things.",[10,3759,3760,3761,3763],{},"Branch because it gives you isolation. You can work on a feature without affecting ",[21,3762,3670],{},", experiment without risk, and throw away a branch if it doesn't work out. Your teammates do the same, and nobody steps on anyone else.",[10,3765,3766,3767,597,3770,597,3773,3776],{},"Name branches descriptively: ",[21,3768,3769],{},"feature\u002Fuser-profile",[21,3771,3772],{},"fix\u002Flogin-redirect",[21,3774,3775],{},"chore\u002Fupdate-deps",". The prefix tells reviewers what kind of change to expect.",[108,3778,3780],{"id":3779},"pull-requests","Pull Requests",[10,3782,3783,3784,3786],{},"A pull request is a request to merge your branch into another branch — usually ",[21,3785,3670],{},". It's where code review happens.",[10,3788,3789],{},"Push your branch to GitHub first:",[14,3791,3793],{"className":1330,"code":3792,"language":1332,"meta":19,"style":19},"git push -u origin feature\u002Flogin\n",[21,3794,3795],{"__ignoreMap":19},[24,3796,3797,3799,3801,3803,3805],{"class":26,"line":27},[24,3798,3275],{"class":389},[24,3800,3611],{"class":350},[24,3802,3614],{"class":3231},[24,3804,3601],{"class":350},[24,3806,3719],{"class":350},[10,3808,3809],{},"Then create the PR on GitHub. Write a clear description: what changed, why it changed, and how to test it. Assign reviewers. Wait for CI checks to pass.",[10,3811,3812],{},"Code review basics: keep PRs small and focused. A PR that changes 50 lines gets reviewed carefully. A PR that changes 500 lines gets skimmed. If you're working on a large feature, break it into smaller PRs that build on each other.",[10,3814,3815],{},"Respond to review feedback by pushing additional commits to the same branch. The PR updates automatically.",[108,3817,3819],{"id":3818},"merging-and-resolving-conflicts","Merging and Resolving Conflicts",[10,3821,3822],{},"When a PR is approved, you merge it. GitHub offers several merge strategies, but the two you'll encounter most:",[10,3824,3825,3828,3829,3831],{},[265,3826,3827],{},"Fast-forward merge"," moves the branch pointer forward when there's no divergence. The history stays linear. This happens when ",[21,3830,3670],{}," hasn't changed since you branched off.",[10,3833,3834,3837],{},[265,3835,3836],{},"Merge commit"," creates a new commit that combines two branches. This happens when both branches have new commits. The merge commit has two parents — one from each branch.",[10,3839,3840],{},"Conflicts happen when two branches change the same lines in the same file. Git can't decide which version to keep, so it marks the file:",[14,3842,3846],{"className":3843,"code":3844,"language":3845,"meta":19,"style":19},"language-diff shiki shiki-themes vitesse-light vitesse-dark","\u003C\u003C\u003C\u003C\u003C\u003C\u003C HEAD (Current Change)\nconst greeting = \"Hello\";\n=======\nconst greeting = \"Hi there\";\n>>>>>>> feature\u002Flogin (Incoming Change)\n","diff",[21,3847,3848,3853,3858,3863,3868],{"__ignoreMap":19},[24,3849,3850],{"class":26,"line":27},[24,3851,3852],{},"\u003C\u003C\u003C\u003C\u003C\u003C\u003C HEAD (Current Change)\n",[24,3854,3855],{"class":26,"line":57},[24,3856,3857],{},"const greeting = \"Hello\";\n",[24,3859,3860],{"class":26,"line":78},[24,3861,3862],{},"=======\n",[24,3864,3865],{"class":26,"line":226},[24,3866,3867],{},"const greeting = \"Hi there\";\n",[24,3869,3870],{"class":26,"line":238},[24,3871,3872],{},">>>>>>> feature\u002Flogin (Incoming Change)\n",[10,3874,3875,3876,3879,3880,3883,3884,3879,3886,3889],{},"Everything between ",[21,3877,3878],{},"\u003C\u003C\u003C\u003C\u003C\u003C\u003C HEAD"," and ",[21,3881,3882],{},"======="," is your current branch. Everything between ",[21,3885,3882],{},[21,3887,3888],{},">>>>>>>"," is the incoming branch. To resolve it, delete the markers, keep the code you want, stage the file, and commit:",[14,3891,3893],{"className":1330,"code":3892,"language":1332,"meta":19,"style":19},"git add src\u002Fgreeting.ts\ngit commit -m \"Resolve greeting conflict\"\n",[21,3894,3895,3904],{"__ignoreMap":19},[24,3896,3897,3899,3901],{"class":26,"line":27},[24,3898,3275],{"class":389},[24,3900,1432],{"class":350},[24,3902,3903],{"class":350}," src\u002Fgreeting.ts\n",[24,3905,3906,3908,3910,3912,3914,3917],{"class":26,"line":57},[24,3907,3275],{"class":389},[24,3909,3490],{"class":350},[24,3911,3493],{"class":3231},[24,3913,3287],{"class":346},[24,3915,3916],{"class":350},"Resolve greeting conflict",[24,3918,3293],{"class":346},[10,3920,3921],{},"If things get messy and you want to start over:",[14,3923,3925],{"className":1330,"code":3924,"language":1332,"meta":19,"style":19},"git merge --abort\n",[21,3926,3927],{"__ignoreMap":19},[24,3928,3929,3931,3934],{"class":26,"line":27},[24,3930,3275],{"class":389},[24,3932,3933],{"class":350}," merge",[24,3935,3936],{"class":3231}," --abort\n",[10,3938,3939],{},"This resets to the state before the merge attempt. No harm done.",[108,3941,3943],{"id":3942},"advanced-git","Advanced Git",[10,3945,3946,3951,3952,3954,3955,3958],{},[265,3947,3948],{},[21,3949,3950],{},"git stash"," shelves your uncommitted changes so you can switch branches without committing half-finished work. ",[21,3953,3950],{}," saves them, ",[21,3956,3957],{},"git stash pop"," restores them. I use this multiple times a day — someone asks me to review their PR, I stash my work, switch branches, then come back.",[10,3960,3961,3966],{},[265,3962,3963],{},[21,3964,3965],{},"git rebase"," replays your branch's commits on top of another branch, producing a linear history. Use rebase for local cleanup before pushing. Use merge for integrating shared branches. The rule: don't rebase commits that other people have based work on.",[10,3968,3969,3974,3975,3977,3978,3981],{},[265,3970,3971],{},[21,3972,3973],{},"git cherry-pick"," applies a specific commit from one branch to another without merging the entire branch. Useful when a bugfix on a feature branch needs to land on ",[21,3976,3670],{}," immediately — ",[21,3979,3980],{},"git cherry-pick abc1234"," copies just that commit.",[10,3983,3984,3989],{},[265,3985,3986],{},[21,3987,3988],{},"git bisect"," performs a binary search through your commit history to find which commit introduced a bug. You mark a known good commit and a known bad commit, and Git walks you through the middle points until it isolates the culprit. On large repositories with hundreds of commits between releases, this saves real time.",[10,3991,3992,3995,3996,3999],{},[265,3993,3994],{},"Interactive rebase"," (",[21,3997,3998],{},"git rebase -i HEAD~5",") lets you edit, squash, reorder, or drop recent commits before pushing. I use this to clean up a messy series of \"WIP\" commits into a coherent history. Squash the fixups, reword the messages, then push a clean branch.",[10,4001,4002],{},"These commands aren't daily drivers for most junior developers, but knowing they exist means you'll reach for them when the situation calls for it instead of working around problems manually.",[1443,4004,4005],{},"html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}",{"title":19,"searchDepth":57,"depth":57,"links":4007},[4008,4009,4010,4011,4012,4013,4014,4015,4016],{"id":3179,"depth":57,"text":3180},{"id":3192,"depth":57,"text":3193},{"id":3329,"depth":57,"text":3330},{"id":3412,"depth":57,"text":3413},{"id":3577,"depth":57,"text":3578},{"id":3663,"depth":57,"text":3664},{"id":3779,"depth":57,"text":3780},{"id":3818,"depth":57,"text":3819},{"id":3942,"depth":57,"text":3943},"2026-01-31","Practical Git knowledge from init to pull request — no jargon, no cheat sheets, just what you need on day one.",{},"\u002Fwritings\u002Fgit-from-zero-to-pull-request-a-practical-guide",{"title":3171,"description":4018},"writings\u002Fgit-from-zero-to-pull-request-a-practical-guide","wyqKKdI4UWfIU-9_6fqKkHwWKRCML-BmdO8mfOKc-gM",{"id":4025,"title":4026,"body":4027,"category":4436,"date":4437,"description":4438,"draft":1462,"extension":1463,"meta":4439,"navigation":378,"path":4440,"seo":4441,"sitemap":378,"stem":4442,"__hash__":4443},"writings\u002Fwritings\u002F2025s-security-crisis-why-memory-safe-languages-matter.md","2025's Security Crisis: Why Memory-Safe Languages Matter",{"type":7,"value":4028,"toc":4414},[4029,4036,4039,4042,4046,4049,4053,4056,4116,4119,4122,4126,4129,4132,4136,4143,4146,4176,4179,4183,4186,4189,4209,4212,4216,4219,4222,4225,4229,4232,4235,4239,4242,4245,4249,4256,4259,4270,4274,4281,4284,4295,4299,4302,4305,4308,4312,4315,4319,4344,4348,4374,4378,4398,4402,4405,4408,4411],[10,4030,4031,4032,4035],{},"I've been watching the security landscape for years, mostly from the sidelines—patching dependencies, running ",[21,4033,4034],{},"npm audit",", and hoping for the best. But the numbers from 2025 made me sit up straight. 48,185 CVEs. In one year. That's 132 new vulnerabilities every single day.",[10,4037,4038],{},"And here's the kicker: 28% of exploits now happen within 24 hours of disclosure.",[10,4040,4041],{},"There's no time to be slow anymore.",[108,4043,4045],{"id":4044},"the-numbers-that-changed-my-perspective","The Numbers That Changed My Perspective",[10,4047,4048],{},"Three major reports dropped recently. Together, they paint a clear picture.",[2795,4050,4052],{"id":4051},"the-bad-news-first","The Bad News First",[10,4054,4055],{},"According to Deepstrike's 2025 analysis:",[4057,4058,4059,4072],"table",{},[4060,4061,4062],"thead",{},[4063,4064,4065,4069],"tr",{},[4066,4067,4068],"th",{},"Metric",[4066,4070,4071],{},"Number",[4073,4074,4075,4084,4092,4100,4108],"tbody",{},[4063,4076,4077,4081],{},[4078,4079,4080],"td",{},"CVEs in H1 2025",[4078,4082,4083],{},"21,500+",[4063,4085,4086,4089],{},[4078,4087,4088],{},"Daily average",[4078,4090,4091],{},"~133 new vulnerabilities",[4063,4093,4094,4097],{},[4078,4095,4096],{},"High\u002FCritical severity",[4078,4098,4099],{},"38%",[4063,4101,4102,4105],{},[4078,4103,4104],{},"Year-end projection",[4078,4106,4107],{},"~50,000 CVEs",[4063,4109,4110,4113],{},[4078,4111,4112],{},"Exploited within 24 hours",[4078,4114,4115],{},"28%",[10,4117,4118],{},"The Linux kernel alone had ~2,879 CVEs. The WordPress ecosystem? Over 6,700 vulnerabilities—and 90% of those came from plugins, not WordPress itself.",[10,4120,4121],{},"That stands out when you consider how many client projects run on WordPress. All those \"trusted\" plugins installed without a second thought — they're the attack surface now.",[2795,4123,4125],{"id":4124},"the-xss-problem-that-wont-die","The XSS Problem That Won't Die",[10,4127,4128],{},"Over 8,000 CVEs in 2025 were cross-site scripting vulnerabilities. XSS. In 2025. A vulnerability class we've known how to prevent for decades.",[10,4130,4131],{},"The Stack's analysis points to the real culprit — WordPress plugins and legacy code built without security oversight. The same bugs from 2005, shipped faster and at scale.",[108,4133,4135],{"id":4134},"the-1000x-improvement-that-made-me-rethink-everything","The 1000x Improvement That Made Me Rethink Everything",[10,4137,4138,4139,4142],{},"While the CVE numbers keep climbing, Google's Android team quietly published something remarkable: ",[265,4140,4141],{},"a 1000x reduction in memory safety vulnerability density"," when comparing Rust to C\u002FC++.",[10,4144,4145],{},"Not 10x. Not 100x. A thousand times fewer vulnerabilities per million lines of code.",[4057,4147,4148,4158],{},[4060,4149,4150],{},[4063,4151,4152,4155],{},[4066,4153,4154],{},"Language",[4066,4156,4157],{},"Vulnerabilities per MLOC",[4073,4159,4160,4168],{},[4063,4161,4162,4165],{},[4078,4163,4164],{},"C\u002FC++",[4078,4166,4167],{},"~1,000",[4063,4169,4170,4173],{},[4078,4171,4172],{},"Rust",[4078,4174,4175],{},"~0.2",[10,4177,4178],{},"And this isn't theoretical—Android now runs about 5 million lines of Rust in production. The platform has expanded Rust into the Linux kernel (Android 6.12 ships the first production Rust driver), firmware, and even apps like Chromium's parsers and MLS messaging protocols.",[2795,4180,4182],{"id":4181},"the-speed-myth-is-dead","The Speed Myth Is Dead",[10,4184,4185],{},"The assumption has always been that safety costs speed — ship fast or ship secure, pick one.",[10,4187,4188],{},"Google's DORA metrics say otherwise:",[1402,4190,4191,4197,4203],{},[1405,4192,4193,4196],{},[265,4194,4195],{},"20% fewer revisions"," needed for Rust changes vs C++",[1405,4198,4199,4202],{},[265,4200,4201],{},"25% faster code review"," time",[1405,4204,4205,4208],{},[265,4206,4207],{},"4x lower rollback rate"," on medium\u002Flarge changes",[10,4210,4211],{},"The safer path is now also the faster one. That's not a tradeoff anymore—it's just better.",[108,4213,4215],{"id":4214},"the-near-miss-that-proves-defense-in-depth-works","The Near-Miss That Proves Defense-in-Depth Works",[10,4217,4218],{},"Google almost shipped their first Rust memory safety vulnerability—a buffer overflow in CrabbyAVIF. The bug made it through code review, testing, everything.",[10,4220,4221],{},"But it never became exploitable. Why? The Scudo hardened memory allocator caught it before it could cause damage.",[10,4223,4224],{},"This is what layered security looks like in practice. Rust catches most issues at compile time. When something slips through, hardened allocators provide the safety net. It's not about being perfect—it's about building systems that fail safely.",[108,4226,4228],{"id":4227},"what-this-means-for-javascript-developers","What This Means for JavaScript Developers",[10,4230,4231],{},"I know what you're thinking: \"Cool story about Rust, but I'm shipping Next.js apps, not kernel drivers.\"",[10,4233,4234],{},"Fair point. But the patterns apply everywhere:",[2795,4236,4238],{"id":4237},"_1-memory-safety-is-becoming-table-stakes","1. Memory Safety Is Becoming Table Stakes",[10,4240,4241],{},"WebAssembly is bringing Rust to the browser. Node.js is getting more Rust-based tooling every day (hello, SWC, Biome, and the entire Vite ecosystem). The line between \"systems programming\" and \"web development\" is blurring.",[10,4243,4244],{},"I've already written about how I prefer Rust-based tools like Biome over their JavaScript equivalents. Speed matters, but so does reliability. Tools that can't crash are tools I trust.",[2795,4246,4248],{"id":4247},"_2-the-patch-window-is-shrinking","2. The Patch Window Is Shrinking",[10,4250,4251,4252,4255],{},"With 28% of exploits happening within 24 hours, ",[21,4253,4254],{},"dependabot"," isn't optional—it's critical infrastructure. That weekly dependency review you keep postponing? It's a security liability.",[10,4257,4258],{},"My current approach:",[1402,4260,4261,4264,4267],{},[1405,4262,4263],{},"Automated dependency updates via Renovate",[1405,4265,4266],{},"Breaking change PRs get reviewed same-day",[1405,4268,4269],{},"Security patches merge immediately (no waiting for \"the next sprint\")",[2795,4271,4273],{"id":4272},"_3-the-plugin-problem-is-universal","3. The Plugin Problem Is Universal",[10,4275,4276,4277,4280],{},"WordPress plugins being the attack surface isn't unique to PHP. Every package you install from npm is a trust decision. Every ",[21,4278,4279],{},"bun add"," is accepting code from strangers.",[10,4282,4283],{},"I've started being more intentional about dependencies:",[1402,4285,4286,4289,4292],{},[1405,4287,4288],{},"Fewer dependencies overall",[1405,4290,4291],{},"Preferring packages with active maintenance and security track records",[1405,4293,4294],{},"Reading the code (or at least the README) before installing",[108,4296,4298],{"id":4297},"the-elephant-in-the-room-ai-accelerated-exploits","The Elephant in the Room: AI-Accelerated Exploits",[10,4300,4301],{},"One line from The Stack's analysis: LLMs are speeding up exploit development using publicly available patch information.",[10,4303,4304],{},"When a CVE drops with a fix, attackers can now use AI to reverse-engineer the vulnerability from the patch diff. The window between \"patch available\" and \"exploit in the wild\" is collapsing.",[10,4306,4307],{},"This changes the game. We can't rely on \"patch eventually\" anymore. It has to be \"patch immediately\" or accept the risk.",[108,4309,4311],{"id":4310},"what-im-actually-doing-about-this","What I'm Actually Doing About This",[10,4313,4314],{},"After digesting all this, here's my updated security posture:",[2795,4316,4318],{"id":4317},"for-new-projects","For New Projects",[1402,4320,4321,4327,4333,4339],{},[1405,4322,4323,4326],{},[265,4324,4325],{},"TypeScript strict mode everywhere","—type safety catches bugs before runtime",[1405,4328,4329,4332],{},[265,4330,4331],{},"Prefer Rust-based tooling"," when available (Biome, SWC, turbo)",[1405,4334,4335,4338],{},[265,4336,4337],{},"Minimize attack surface","—fewer dependencies, more intentional choices",[1405,4340,4341],{},[265,4342,4343],{},"Automated updates with same-day security patches",[2795,4345,4347],{"id":4346},"for-existing-projects","For Existing Projects",[1402,4349,4350,4356,4362,4368],{},[1405,4351,4352,4355],{},[265,4353,4354],{},"Audit the dependency tree","—what's actually running in production?",[1405,4357,4358,4361],{},[265,4359,4360],{},"Remove unused packages","—if it's not used, it shouldn't be installed",[1405,4363,4364,4367],{},[265,4365,4366],{},"Enable automated security scanning","—GitHub's Dependabot, Snyk, whatever works",[1405,4369,4370,4373],{},[265,4371,4372],{},"Review plugin\u002Fextension choices","—especially for CMS-based projects",[2795,4375,4377],{"id":4376},"for-the-team","For the Team",[1402,4379,4380,4386,4392],{},[1405,4381,4382,4385],{},[265,4383,4384],{},"Security reviews as part of PR process","—not a separate gate",[1405,4387,4388,4391],{},[265,4389,4390],{},"Shared understanding of common vulnerabilities","—XSS, SQL injection, the basics",[1405,4393,4394,4397],{},[265,4395,4396],{},"Incident response plan","—what happens when (not if) something gets through?",[108,4399,4401],{"id":4400},"the-bottom-line","The Bottom Line",[10,4403,4404],{},"Two trends define 2025's security landscape: vulnerability counts are exploding, and memory-safe languages can make a 1000x difference. The tools are getting better. The path forward is clear.",[10,4406,4407],{},"Google didn't wait for a catastrophic Android vulnerability to invest in Rust. They saw the trend lines and made a choice.",[10,4409,4410],{},"Start treating security as a developer experience problem. Safer tools should also be faster tools. Security reviews should be part of shipping, not a blocker to it. Memory-safe languages aren't just for systems programmers anymore — they're becoming the foundation everything else builds on.",[10,4412,4413],{},"The 1000x improvement is real. The exploit windows are shrinking. The only question is whether we adapt fast enough.",{"title":19,"searchDepth":57,"depth":57,"links":4415},[4416,4420,4423,4424,4429,4430,4435],{"id":4044,"depth":57,"text":4045,"children":4417},[4418,4419],{"id":4051,"depth":78,"text":4052},{"id":4124,"depth":78,"text":4125},{"id":4134,"depth":57,"text":4135,"children":4421},[4422],{"id":4181,"depth":78,"text":4182},{"id":4214,"depth":57,"text":4215},{"id":4227,"depth":57,"text":4228,"children":4425},[4426,4427,4428],{"id":4237,"depth":78,"text":4238},{"id":4247,"depth":78,"text":4248},{"id":4272,"depth":78,"text":4273},{"id":4297,"depth":57,"text":4298},{"id":4310,"depth":57,"text":4311,"children":4431},[4432,4433,4434],{"id":4317,"depth":78,"text":4318},{"id":4346,"depth":78,"text":4347},{"id":4376,"depth":78,"text":4377},{"id":4400,"depth":57,"text":4401},"essay","2026-01-18","48,185 CVEs in one year, exploits within hours, and a 1000x improvement from Rust. The numbers that changed how I think about security.",{},"\u002Fwritings\u002F2025s-security-crisis-why-memory-safe-languages-matter",{"title":4026,"description":4438},"writings\u002F2025s-security-crisis-why-memory-safe-languages-matter","CucPUAWs8T6kKzC9PEKBfflrxR2MhJ8m73qRtpZgygs",{"id":4445,"title":4446,"body":4447,"category":3161,"date":4828,"description":4829,"draft":1462,"extension":1463,"meta":4830,"navigation":378,"path":4831,"seo":4832,"sitemap":378,"stem":4833,"__hash__":4834},"writings\u002Fwritings\u002Fmy-pragmatic-approach-to-building-npm-packages-in-2026.md","My Pragmatic Approach to Building npm Packages in 2026",{"type":7,"value":4448,"toc":4817},[4449,4452,4455,4459,4462,4465,4468,4471,4475,4478,4532,4535,4538,4541,4654,4657,4661,4671,4677,4683,4689,4692,4696,4702,4708,4714,4720,4723,4727,4730,4733,4744,4747,4751,4754,4757,4760,4763,4767,4773,4776,4782,4785,4789,4792,4795,4798,4802,4805,4808,4811,4814],[10,4450,4451],{},"A few years ago, shipping a TypeScript package to npm felt like assembling furniture without instructions. You'd spend the first day configuring tools, the second day fighting ESM\u002FCJS compatibility, and maybe—if you were lucky—the third day actually writing code.",[10,4453,4454],{},"I've shipped enough packages now to know what matters and what doesn't. Spoiler: most of what I used to configure manually was a waste of time.",[108,4456,4458],{"id":4457},"what-changed","What Changed",[10,4460,4461],{},"In 2023, my typical package setup involved: TypeScript with a custom tsconfig, ESLint with Prettier, Jest for testing, Rollup or esbuild for bundling, semantic-release for versioning, and a handful of shell scripts to glue everything together. Seven tools minimum, each with their own configuration file, their own update cycle, their own breaking changes.",[10,4463,4464],{},"I don't do that anymore.",[10,4466,4467],{},"The shift happened gradually. I started noticing a pattern: the projects I maintained longest were the ones with the fewest configuration files. The projects that rotted fastest were the ones where I'd spent days perfecting the \"ideal\" setup.",[10,4469,4470],{},"Configuration is debt. Every config file is a future merge conflict, a potential breaking change, a thing you'll have to remember when you revisit the project in six months.",[108,4472,4474],{"id":4473},"the-tools-i-actually-use","The Tools I Actually Use",[10,4476,4477],{},"My current stack for npm packages:",[4057,4479,4480,4490],{},[4060,4481,4482],{},[4063,4483,4484,4487],{},[4066,4485,4486],{},"Tool",[4066,4488,4489],{},"Purpose",[4073,4491,4492,4500,4508,4516,4524],{},[4063,4493,4494,4497],{},[4078,4495,4496],{},"tsdown",[4078,4498,4499],{},"Build ESM and CJS with types",[4063,4501,4502,4505],{},[4078,4503,4504],{},"Biome",[4078,4506,4507],{},"Linting and formatting",[4063,4509,4510,4513],{},[4078,4511,4512],{},"Vitest",[4078,4514,4515],{},"Testing with coverage",[4063,4517,4518,4521],{},[4078,4519,4520],{},"Changesets",[4078,4522,4523],{},"Versioning and changelogs",[4063,4525,4526,4529],{},[4078,4527,4528],{},"Lefthook",[4078,4530,4531],{},"Git hooks",[10,4533,4534],{},"That's it. Five tools. Most of them need less than ten lines of configuration.",[10,4536,4537],{},"The consolidation principle I learned from Python's Ruff applies here too: if one tool can do what three tools did, use the one tool. Biome replaced ESLint plus Prettier for me. It's faster, the config is smaller, and I stopped having arguments with myself about semicolons.",[10,4539,4540],{},"tsdown is the build tool I wish I'd found earlier. Eight lines of config:",[14,4542,4546],{"className":4543,"code":4544,"language":4545,"meta":19,"style":19},"language-typescript shiki shiki-themes vitesse-light vitesse-dark","import { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n  entry: [\"src\u002Findex.ts\"],\n  format: [\"esm\", \"cjs\"],\n  dts: true,\n  clean: true,\n});\n","typescript",[21,4547,4548,4570,4574,4584,4600,4625,4638,4649],{"__ignoreMap":19},[24,4549,4550,4552,4554,4556,4558,4560,4562,4564,4567],{"class":26,"line":27},[24,4551,31],{"class":30},[24,4553,334],{"class":34},[24,4555,683],{"class":44},[24,4557,340],{"class":34},[24,4559,343],{"class":30},[24,4561,3287],{"class":346},[24,4563,4496],{"class":350},[24,4565,4566],{"class":346},"\"",[24,4568,4569],{"class":34},";\n",[24,4571,4572],{"class":26,"line":57},[24,4573,379],{"emptyLinePlaceholder":378},[24,4575,4576,4578,4580,4582],{"class":26,"line":78},[24,4577,148],{"class":30},[24,4579,386],{"class":30},[24,4581,683],{"class":389},[24,4583,392],{"class":34},[24,4585,4586,4589,4591,4593,4596,4598],{"class":26,"line":226},[24,4587,4588],{"class":38},"  entry",[24,4590,716],{"class":34},[24,4592,4566],{"class":346},[24,4594,4595],{"class":350},"src\u002Findex.ts",[24,4597,4566],{"class":346},[24,4599,1208],{"class":34},[24,4601,4602,4605,4607,4609,4612,4614,4616,4618,4621,4623],{"class":26,"line":238},[24,4603,4604],{"class":38},"  format",[24,4606,716],{"class":34},[24,4608,4566],{"class":346},[24,4610,4611],{"class":350},"esm",[24,4613,4566],{"class":346},[24,4615,597],{"class":34},[24,4617,4566],{"class":346},[24,4619,4620],{"class":350},"cjs",[24,4622,4566],{"class":346},[24,4624,1208],{"class":34},[24,4626,4627,4630,4632,4635],{"class":26,"line":244},[24,4628,4629],{"class":38},"  dts",[24,4631,220],{"class":34},[24,4633,4634],{"class":30},"true",[24,4636,4637],{"class":34},",\n",[24,4639,4640,4643,4645,4647],{"class":26,"line":403},[24,4641,4642],{"class":38},"  clean",[24,4644,220],{"class":34},[24,4646,4634],{"class":30},[24,4648,4637],{"class":34},[24,4650,4651],{"class":26,"line":422},[24,4652,4653],{"class":34},"});\n",[10,4655,4656],{},"Dual output, type declarations, done. I don't think about bundling anymore.",[108,4658,4660],{"id":4659},"what-i-refuse-to-configure","What I Refuse to Configure",[10,4662,4663,4666,4667,4670],{},[265,4664,4665],{},"Strictness."," My TypeScript config extends ",[21,4668,4669],{},"@tsconfig\u002Fstrictest"," and I don't relax it. Ever. I've been bitten too many times by \"temporarily\" disabling strict null checks and then shipping a bug that would've been caught at compile time.",[10,4672,4673,4676],{},[265,4674,4675],{},"Coverage thresholds."," 80% minimum, enforced in CI. Not because 80% is magical, but because having a floor prevents the slow decay where coverage drops 1% per month until you're at 40% and afraid to touch anything.",[10,4678,4679,4682],{},[265,4680,4681],{},"JSDoc for public APIs."," ESLint enforces this. Every exported function needs documentation. Not because I love writing docs—I don't—but because future me will forget what the function does, and the compile-time error is easier to deal with than the runtime confusion.",[10,4684,4685,4688],{},[265,4686,4687],{},"Import sorting."," Biome handles this automatically. I don't care what order the imports are in. I care that they're consistent. Automation means I never think about it.",[10,4690,4691],{},"The common thread: anything that can be automated should be automated. Anything that can be enforced should be enforced. The less I have to remember, the less I forget.",[108,4693,4695],{"id":4694},"what-i-intentionally-dont-support","What I Intentionally Don't Support",[10,4697,4698,4701],{},[265,4699,4700],{},"Anything below Node 20."," Maintaining compatibility with older Node versions isn't free. Every polyfill is complexity. Every version matrix is CI time. I pick a floor and stick with it.",[10,4703,4704,4707],{},[265,4705,4706],{},"Monorepos from day one."," I went through a phase where everything had to be a monorepo with shared packages and workspace protocols. It was intellectually satisfying and practically miserable. Now I start with a single package. If it genuinely needs to split, I split it. Most packages never need to split.",[10,4709,4710,4713],{},[265,4711,4712],{},"Custom build pipelines."," If tsdown can't handle it, I reconsider whether the package should exist in that form. Ejecting into custom esbuild or Rollup configs has never once improved my life in the long run.",[10,4715,4716,4719],{},[265,4717,4718],{},"Optional strictness."," I've seen codebases with \"strict mode optional\" and watched as every new contributor disabled another check because it was inconvenient. Strictness is the default or it doesn't exist.",[10,4721,4722],{},"These aren't universal principles. They're my principles, for my projects, based on what I've learned maintaining code over years. Your constraints might be different.",[108,4724,4726],{"id":4725},"the-maintenance-test","The Maintenance Test",[10,4728,4729],{},"Setup time is a trap. It's easy to obsess over the perfect initial configuration because that's the part that feels productive. But most of a package's life is maintenance: dependency updates, bug fixes, occasional features.",[10,4731,4732],{},"I evaluate tooling choices by asking: what happens in two years?",[1402,4734,4735,4738,4741],{},[1405,4736,4737],{},"Will this tool still exist? (Biome, Vitest: likely. Random ESLint plugin with 200 GitHub stars: maybe not.)",[1405,4739,4740],{},"Will the config still work after major version bumps? (Fewer config files means fewer breaking changes to manage.)",[1405,4742,4743],{},"Can someone else contribute without studying the build system? (If the README needs a section explaining the build process, something is wrong.)",[10,4745,4746],{},"The packages I'm proudest of are the ones where I can merge a PR without thinking about infrastructure. The build just works. The tests just run. The release just publishes.",[108,4748,4750],{"id":4749},"speed-matters-more-than-i-thought","Speed Matters More Than I Thought",[10,4752,4753],{},"I used to think build speed was a nice-to-have. Now I think it's essential.",[10,4755,4756],{},"Fast builds mean faster feedback loops. Faster feedback loops mean I catch mistakes earlier. Catching mistakes earlier means I ship fewer bugs. It's not about saving thirty seconds—it's about staying in flow.",[10,4758,4759],{},"Biome lints my codebase in milliseconds. Vitest runs tests faster than Jest ever did. tsdown builds in under a second. The cumulative effect is that I actually run these tools. When the lint took ten seconds, I'd skip it \"just this once.\" When it takes 200ms, I run it on every save.",[10,4761,4762],{},"This is why I prefer Rust-based tools when they exist. Not because Rust is trendy, but because the speed difference changes my behavior.",[108,4764,4766],{"id":4765},"the-single-export-surface","The Single Export Surface",[10,4768,4769,4770,4772],{},"Every package I build has one entry point: ",[21,4771,4595],{},". Everything public gets exported from there. Everything internal stays internal.",[10,4774,4775],{},"This constraint seemed limiting at first. What if I need multiple entry points? What about tree-shaking optimization?",[10,4777,4778,4779,35],{},"In practice, it simplifies everything. Consumers know exactly where to import from. The public API is visible in one file. Breaking changes are obvious because they require touching ",[21,4780,4781],{},"index.ts",[10,4783,4784],{},"When a package genuinely needs multiple entry points, that's usually a sign it should be multiple packages. I've been wrong about this exactly once in the last three years.",[108,4786,4788],{"id":4787},"what-im-still-figuring-out","What I'm Still Figuring Out",[10,4790,4791],{},"Documentation beyond JSDoc. I use VitePress for some packages, but the maintenance burden is real. Auto-generated API docs from types are never quite good enough. Hand-written guides get stale. I haven't found the right balance yet.",[10,4793,4794],{},"Changelogs. Changesets generate them automatically, which is better than nothing, but the output is mechanical. Good changelogs tell a story. I'm not sure how to automate that.",[10,4796,4797],{},"Monorepo avoidance has limits. Some projects genuinely benefit from shared tooling and atomic commits across packages. I just haven't found a monorepo setup that doesn't feel like fighting the tools.",[108,4799,4801],{"id":4800},"the-point","The Point",[10,4803,4804],{},"Building npm packages in 2026 is easier than it was in 2020, but only if you resist the urge to configure everything. The best setup is the one you don't think about.",[10,4806,4807],{},"My approach: strict defaults enforced by fast tools, minimal configuration, aggressive consolidation. Five dependencies for the dev toolchain. One tsconfig. One linter config. Build, test, ship.",[10,4809,4810],{},"It's not clever. It won't win any awards. But the packages ship, they work, and I can still understand them a year later.",[10,4812,4813],{},"That's enough.",[1443,4815,4816],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":19,"searchDepth":57,"depth":57,"links":4818},[4819,4820,4821,4822,4823,4824,4825,4826,4827],{"id":4457,"depth":57,"text":4458},{"id":4473,"depth":57,"text":4474},{"id":4659,"depth":57,"text":4660},{"id":4694,"depth":57,"text":4695},{"id":4725,"depth":57,"text":4726},{"id":4749,"depth":57,"text":4750},{"id":4765,"depth":57,"text":4766},{"id":4787,"depth":57,"text":4788},{"id":4800,"depth":57,"text":4801},"2026-01-08","Five tools, minimal config, strict defaults. The npm package setup I actually maintain.",{},"\u002Fwritings\u002Fmy-pragmatic-approach-to-building-npm-packages-in-2026",{"title":4446,"description":4829},"writings\u002Fmy-pragmatic-approach-to-building-npm-packages-in-2026","MFgjQkL6szehSruA8kd10LCTvau5FVBQFsMvRga3zus",{"id":4836,"title":4837,"body":4838,"category":1459,"date":6107,"description":6108,"draft":1462,"extension":1463,"meta":6109,"navigation":378,"path":6110,"seo":6111,"sitemap":378,"stem":6112,"__hash__":6113},"writings\u002Fwritings\u002Ffrom-requirements-txt-to-pyproject-toml-python-evolution.md","From requirements.txt to pyproject.toml: Python Evolution",{"type":7,"value":4839,"toc":6093},[4840,4847,4853,4857,4860,4909,4912,4930,4933,4937,4943,4946,5077,5095,5105,5109,5112,5240,5247,5254,5258,5261,5368,5383,5390,5394,5397,5553,5560,5563,5567,5570,5576,5579,5583,5586,5654,5667,5671,5674,5796,5799,5803,5806,5927,5930,5934,5937,5982,5989,5993,6073,6077,6080,6090],[10,4841,4842,4843,4846],{},"A few months ago, I wrote about venturing into Python territory as a Node.js developer. That post was about culture shock — learning ",[21,4844,4845],{},"requirements.txt",", virtual environments, and the Python \"trinity\" of Black, Flake8, and MyPy. This is the sequel. I've shipped multiple Python projects since then, and my setup has evolved dramatically.",[10,4848,4849,4850,4852],{},"Spoiler: I don't use ",[21,4851,4845],{}," anymore. Or Black. Or Flake8. Here's what changed.",[108,4854,4856],{"id":4855},"the-old-way-vs-the-new-way","The Old Way vs. The New Way",[10,4858,4859],{},"Remember my original setup?",[14,4861,4863],{"className":1330,"code":4862,"language":1332,"meta":19,"style":19},"# The old way (what I wrote about before)\npython -m venv venv\nsource venv\u002Fbin\u002Factivate\npip install -r requirements.txt -r requirements-dev.txt\n",[21,4864,4865,4870,4883,4891],{"__ignoreMap":19},[24,4866,4867],{"class":26,"line":27},[24,4868,4869],{"class":53},"# The old way (what I wrote about before)\n",[24,4871,4872,4875,4877,4880],{"class":26,"line":57},[24,4873,4874],{"class":389},"python",[24,4876,3493],{"class":3231},[24,4878,4879],{"class":350}," venv",[24,4881,4882],{"class":350}," venv\n",[24,4884,4885,4888],{"class":26,"line":78},[24,4886,4887],{"class":38},"source",[24,4889,4890],{"class":350}," venv\u002Fbin\u002Factivate\n",[24,4892,4893,4896,4898,4901,4904,4906],{"class":26,"line":226},[24,4894,4895],{"class":389},"pip",[24,4897,3209],{"class":350},[24,4899,4900],{"class":3231}," -r",[24,4902,4903],{"class":350}," requirements.txt",[24,4905,4900],{"class":3231},[24,4907,4908],{"class":350}," requirements-dev.txt\n",[10,4910,4911],{},"Now? A single command:",[14,4913,4915],{"className":1330,"code":4914,"language":1332,"meta":19,"style":19},"# The new way\nuv sync\n",[21,4916,4917,4922],{"__ignoreMap":19},[24,4918,4919],{"class":26,"line":27},[24,4920,4921],{"class":53},"# The new way\n",[24,4923,4924,4927],{"class":26,"line":57},[24,4925,4926],{"class":389},"uv",[24,4928,4929],{"class":350}," sync\n",[10,4931,4932],{},"That's it. One tool. One command. Everything just works.",[108,4934,4936],{"id":4935},"uv-the-package-manager-that-changed-everything","uv: The Package Manager That Changed Everything",[10,4938,4939,4940,4942],{},"If you've used Bun in Node.js, ",[265,4941,4926],{}," is the equivalent for Python — same speed improvement, same simplicity.",[10,4944,4945],{},"Here's my actual setup from a recent project:",[14,4947,4951],{"className":4948,"code":4949,"language":4950,"meta":19,"style":19},"language-toml shiki shiki-themes vitesse-light vitesse-dark","[project]\nname = \"load-tester\"\nversion = \"0.1.0\"\ndescription = \"A high-performance async load testing tool\"\nrequires-python = \">=3.12\"\ndependencies = [\n    \"aiohttp>=3.13.2\",\n    \"pydantic>=2.12.5\",\n    \"rich>=14.2.0\",\n    \"typer>=0.20.0\",\n]\n\n[dependency-groups]\ndev = [\n    \"pytest>=8.0.0\",\n    \"pytest-asyncio>=0.23.0\",\n    \"pytest-cov>=4.1.0\",\n    \"ruff>=0.1.0\",\n    \"mypy>=1.8.0\",\n]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n","toml",[21,4952,4953,4958,4963,4968,4973,4978,4983,4988,4993,4998,5003,5008,5012,5017,5022,5027,5032,5037,5043,5049,5054,5059,5065,5071],{"__ignoreMap":19},[24,4954,4955],{"class":26,"line":27},[24,4956,4957],{},"[project]\n",[24,4959,4960],{"class":26,"line":57},[24,4961,4962],{},"name = \"load-tester\"\n",[24,4964,4965],{"class":26,"line":78},[24,4966,4967],{},"version = \"0.1.0\"\n",[24,4969,4970],{"class":26,"line":226},[24,4971,4972],{},"description = \"A high-performance async load testing tool\"\n",[24,4974,4975],{"class":26,"line":238},[24,4976,4977],{},"requires-python = \">=3.12\"\n",[24,4979,4980],{"class":26,"line":244},[24,4981,4982],{},"dependencies = [\n",[24,4984,4985],{"class":26,"line":403},[24,4986,4987],{},"    \"aiohttp>=3.13.2\",\n",[24,4989,4990],{"class":26,"line":422},[24,4991,4992],{},"    \"pydantic>=2.12.5\",\n",[24,4994,4995],{"class":26,"line":453},[24,4996,4997],{},"    \"rich>=14.2.0\",\n",[24,4999,5000],{"class":26,"line":490},[24,5001,5002],{},"    \"typer>=0.20.0\",\n",[24,5004,5005],{"class":26,"line":496},[24,5006,5007],{},"]\n",[24,5009,5010],{"class":26,"line":504},[24,5011,379],{"emptyLinePlaceholder":378},[24,5013,5014],{"class":26,"line":520},[24,5015,5016],{},"[dependency-groups]\n",[24,5018,5019],{"class":26,"line":545},[24,5020,5021],{},"dev = [\n",[24,5023,5024],{"class":26,"line":571},[24,5025,5026],{},"    \"pytest>=8.0.0\",\n",[24,5028,5029],{"class":26,"line":640},[24,5030,5031],{},"    \"pytest-asyncio>=0.23.0\",\n",[24,5033,5034],{"class":26,"line":645},[24,5035,5036],{},"    \"pytest-cov>=4.1.0\",\n",[24,5038,5040],{"class":26,"line":5039},18,[24,5041,5042],{},"    \"ruff>=0.1.0\",\n",[24,5044,5046],{"class":26,"line":5045},19,[24,5047,5048],{},"    \"mypy>=1.8.0\",\n",[24,5050,5052],{"class":26,"line":5051},20,[24,5053,5007],{},[24,5055,5057],{"class":26,"line":5056},21,[24,5058,379],{"emptyLinePlaceholder":378},[24,5060,5062],{"class":26,"line":5061},22,[24,5063,5064],{},"[build-system]\n",[24,5066,5068],{"class":26,"line":5067},23,[24,5069,5070],{},"requires = [\"hatchling\"]\n",[24,5072,5074],{"class":26,"line":5073},24,[24,5075,5076],{},"build-backend = \"hatchling.build\"\n",[10,5078,5079,5080,5082,5083,5086,5087,5090,5091,5094],{},"No more ",[21,5081,4845],{},". No more ",[21,5084,5085],{},"requirements-dev.txt",". Everything lives in ",[21,5088,5089],{},"pyproject.toml","—just like ",[21,5092,5093],{},"package.json"," in Node.js, but better structured.",[10,5096,3622,5097,5100,5101,5104],{},[21,5098,5099],{},"[dependency-groups]"," feature is well-designed. Development dependencies stay separate from production, but they're all in one file. When I run ",[21,5102,5103],{},"uv sync",", it sets up everything. When I deploy, I can exclude dev dependencies.",[108,5106,5108],{"id":5107},"ruff-one-tool-to-rule-them-all","Ruff: One Tool to Rule Them All",[10,5110,5111],{},"Remember the \"Python Development Trinity\" I mentioned before—Black, Flake8, and MyPy? I've collapsed two of those into one:",[14,5113,5115],{"className":4948,"code":5114,"language":4950,"meta":19,"style":19},"[tool.ruff]\nline-length = 88\ntarget-version = \"py312\"\nsrc = [\"src\", \"tests\"]\n\n[tool.ruff.lint]\nselect = [\n    \"E\",   # pycodestyle errors\n    \"W\",   # pycodestyle warnings\n    \"F\",   # Pyflakes\n    \"I\",   # isort\n    \"B\",   # flake8-bugbear\n    \"C4\",  # flake8-comprehensions\n    \"UP\",  # pyupgrade\n    \"ARG\", # flake8-unused-arguments\n    \"SIM\", # flake8-simplify\n    \"TCH\", # flake8-type-checking\n    \"PTH\", # flake8-use-pathlib\n    \"RUF\", # Ruff-specific rules\n]\n\n[tool.ruff.format]\nquote-style = \"double\"\nindent-style = \"space\"\ndocstring-code-format = true\n",[21,5116,5117,5122,5127,5132,5137,5141,5146,5151,5156,5161,5166,5171,5176,5181,5186,5191,5196,5201,5206,5211,5215,5219,5224,5229,5234],{"__ignoreMap":19},[24,5118,5119],{"class":26,"line":27},[24,5120,5121],{},"[tool.ruff]\n",[24,5123,5124],{"class":26,"line":57},[24,5125,5126],{},"line-length = 88\n",[24,5128,5129],{"class":26,"line":78},[24,5130,5131],{},"target-version = \"py312\"\n",[24,5133,5134],{"class":26,"line":226},[24,5135,5136],{},"src = [\"src\", \"tests\"]\n",[24,5138,5139],{"class":26,"line":238},[24,5140,379],{"emptyLinePlaceholder":378},[24,5142,5143],{"class":26,"line":244},[24,5144,5145],{},"[tool.ruff.lint]\n",[24,5147,5148],{"class":26,"line":403},[24,5149,5150],{},"select = [\n",[24,5152,5153],{"class":26,"line":422},[24,5154,5155],{},"    \"E\",   # pycodestyle errors\n",[24,5157,5158],{"class":26,"line":453},[24,5159,5160],{},"    \"W\",   # pycodestyle warnings\n",[24,5162,5163],{"class":26,"line":490},[24,5164,5165],{},"    \"F\",   # Pyflakes\n",[24,5167,5168],{"class":26,"line":496},[24,5169,5170],{},"    \"I\",   # isort\n",[24,5172,5173],{"class":26,"line":504},[24,5174,5175],{},"    \"B\",   # flake8-bugbear\n",[24,5177,5178],{"class":26,"line":520},[24,5179,5180],{},"    \"C4\",  # flake8-comprehensions\n",[24,5182,5183],{"class":26,"line":545},[24,5184,5185],{},"    \"UP\",  # pyupgrade\n",[24,5187,5188],{"class":26,"line":571},[24,5189,5190],{},"    \"ARG\", # flake8-unused-arguments\n",[24,5192,5193],{"class":26,"line":640},[24,5194,5195],{},"    \"SIM\", # flake8-simplify\n",[24,5197,5198],{"class":26,"line":645},[24,5199,5200],{},"    \"TCH\", # flake8-type-checking\n",[24,5202,5203],{"class":26,"line":5039},[24,5204,5205],{},"    \"PTH\", # flake8-use-pathlib\n",[24,5207,5208],{"class":26,"line":5045},[24,5209,5210],{},"    \"RUF\", # Ruff-specific rules\n",[24,5212,5213],{"class":26,"line":5051},[24,5214,5007],{},[24,5216,5217],{"class":26,"line":5056},[24,5218,379],{"emptyLinePlaceholder":378},[24,5220,5221],{"class":26,"line":5061},[24,5222,5223],{},"[tool.ruff.format]\n",[24,5225,5226],{"class":26,"line":5067},[24,5227,5228],{},"quote-style = \"double\"\n",[24,5230,5231],{"class":26,"line":5073},[24,5232,5233],{},"indent-style = \"space\"\n",[24,5235,5237],{"class":26,"line":5236},25,[24,5238,5239],{},"docstring-code-format = true\n",[10,5241,5242,5243,5246],{},"Ruff does what Black + Flake8 + isort did, but in a single tool that's ",[265,5244,5245],{},"written in Rust"," and runs in milliseconds. My entire codebase lints in the time it took Flake8 to start up.",[10,5248,5249,5250,5253],{},"Ruff's ",[21,5251,5252],{},"select"," system lets me pick exactly which rules I want. I'm not stuck with a monolithic configuration—I can enable flake8-bugbear for catching common bugs, flake8-simplify for code simplification suggestions, and pyupgrade for automatically modernizing my code.",[108,5255,5257],{"id":5256},"pydantic-typescript-level-confidence","Pydantic: TypeScript-Level Confidence",[10,5259,5260],{},"Coming from TypeScript, I missed knowing data shapes at compile time. Pydantic gives me that in Python:",[14,5262,5265],{"className":5263,"code":5264,"language":4874,"meta":19,"style":19},"language-python shiki shiki-themes vitesse-light vitesse-dark","from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator\nfrom typing import Annotated\n\nclass LoadTestConfig(BaseModel):\n    \"\"\"Configuration for a single load test execution.\"\"\"\n\n    model_config = ConfigDict(frozen=True)\n\n    url: HttpUrl\n    method: HttpMethod = HttpMethod.GET\n    num_requests: Annotated[int, Field(ge=1)] = 100\n    concurrency: Annotated[int, Field(ge=1)] = 10\n    timeout: Annotated[float, Field(gt=0)] = 30.0\n    headers: dict[str, str] = Field(default_factory=dict)\n\n    @model_validator(mode=\"after\")\n    def validate_config(self) -> \"LoadTestConfig\":\n        \"\"\"Validate configuration constraints.\"\"\"\n        if self.concurrency > self.num_requests:\n            raise ValueError(\"concurrency cannot exceed num_requests\")\n        return self\n",[21,5266,5267,5272,5277,5281,5286,5291,5295,5300,5304,5309,5314,5319,5324,5329,5334,5338,5343,5348,5353,5358,5363],{"__ignoreMap":19},[24,5268,5269],{"class":26,"line":27},[24,5270,5271],{},"from pydantic import BaseModel, ConfigDict, Field, HttpUrl, model_validator\n",[24,5273,5274],{"class":26,"line":57},[24,5275,5276],{},"from typing import Annotated\n",[24,5278,5279],{"class":26,"line":78},[24,5280,379],{"emptyLinePlaceholder":378},[24,5282,5283],{"class":26,"line":226},[24,5284,5285],{},"class LoadTestConfig(BaseModel):\n",[24,5287,5288],{"class":26,"line":238},[24,5289,5290],{},"    \"\"\"Configuration for a single load test execution.\"\"\"\n",[24,5292,5293],{"class":26,"line":244},[24,5294,379],{"emptyLinePlaceholder":378},[24,5296,5297],{"class":26,"line":403},[24,5298,5299],{},"    model_config = ConfigDict(frozen=True)\n",[24,5301,5302],{"class":26,"line":422},[24,5303,379],{"emptyLinePlaceholder":378},[24,5305,5306],{"class":26,"line":453},[24,5307,5308],{},"    url: HttpUrl\n",[24,5310,5311],{"class":26,"line":490},[24,5312,5313],{},"    method: HttpMethod = HttpMethod.GET\n",[24,5315,5316],{"class":26,"line":496},[24,5317,5318],{},"    num_requests: Annotated[int, Field(ge=1)] = 100\n",[24,5320,5321],{"class":26,"line":504},[24,5322,5323],{},"    concurrency: Annotated[int, Field(ge=1)] = 10\n",[24,5325,5326],{"class":26,"line":520},[24,5327,5328],{},"    timeout: Annotated[float, Field(gt=0)] = 30.0\n",[24,5330,5331],{"class":26,"line":545},[24,5332,5333],{},"    headers: dict[str, str] = Field(default_factory=dict)\n",[24,5335,5336],{"class":26,"line":571},[24,5337,379],{"emptyLinePlaceholder":378},[24,5339,5340],{"class":26,"line":640},[24,5341,5342],{},"    @model_validator(mode=\"after\")\n",[24,5344,5345],{"class":26,"line":645},[24,5346,5347],{},"    def validate_config(self) -> \"LoadTestConfig\":\n",[24,5349,5350],{"class":26,"line":5039},[24,5351,5352],{},"        \"\"\"Validate configuration constraints.\"\"\"\n",[24,5354,5355],{"class":26,"line":5045},[24,5356,5357],{},"        if self.concurrency > self.num_requests:\n",[24,5359,5360],{"class":26,"line":5051},[24,5361,5362],{},"            raise ValueError(\"concurrency cannot exceed num_requests\")\n",[24,5364,5365],{"class":26,"line":5056},[24,5366,5367],{},"        return self\n",[10,5369,5370,5371,5374,5375,5378,5379,5382],{},"This isn't just type hints—it's ",[265,5372,5373],{},"runtime validation"," with clear error messages. The ",[21,5376,5377],{},"Annotated[int, Field(ge=1)]"," ensures the value is at least 1. The ",[21,5380,5381],{},"@model_validator"," handles cross-field validation that TypeScript's type system can't express.",[10,5384,5385,5386,5389],{},"And ",[21,5387,5388],{},"frozen=True","? That makes the model immutable after creation. No accidental mutations. No debugging weird state changes.",[108,5391,5393],{"id":5392},"typer-rich-beautiful-clis-without-the-boilerplate","Typer + Rich: Beautiful CLIs Without the Boilerplate",[10,5395,5396],{},"CLI tools in Python used to mean argparse. Typer with Rich is a significant improvement:",[14,5398,5400],{"className":5263,"code":5399,"language":4874,"meta":19,"style":19},"from typing import Annotated\nimport typer\nfrom rich.console import Console\n\napp = typer.Typer(\n    name=\"load-tester\",\n    help=\"A high-performance async load testing tool.\",\n    no_args_is_help=True,\n)\n\nconsole = Console()\n\n@app.command()\ndef run(\n    url: Annotated[str, typer.Argument(help=\"Target URL for load testing.\")],\n    num_requests: Annotated[\n        int,\n        typer.Option(\n            \"-n\", \"--requests\",\n            help=\"Total number of requests to send.\",\n            min=1,\n        ),\n    ] = 100,\n    verbose: Annotated[\n        bool,\n        typer.Option(\"-v\", \"--verbose\", help=\"Enable verbose output.\"),\n    ] = False,\n) -> None:\n    \"\"\"Run a load test against a target URL.\"\"\"\n    console.print(f\"[green]Testing {url}...[\u002Fgreen]\")\n",[21,5401,5402,5406,5411,5416,5420,5425,5430,5435,5440,5445,5449,5454,5458,5463,5468,5473,5478,5483,5488,5493,5498,5503,5508,5513,5518,5523,5529,5535,5541,5547],{"__ignoreMap":19},[24,5403,5404],{"class":26,"line":27},[24,5405,5276],{},[24,5407,5408],{"class":26,"line":57},[24,5409,5410],{},"import typer\n",[24,5412,5413],{"class":26,"line":78},[24,5414,5415],{},"from rich.console import Console\n",[24,5417,5418],{"class":26,"line":226},[24,5419,379],{"emptyLinePlaceholder":378},[24,5421,5422],{"class":26,"line":238},[24,5423,5424],{},"app = typer.Typer(\n",[24,5426,5427],{"class":26,"line":244},[24,5428,5429],{},"    name=\"load-tester\",\n",[24,5431,5432],{"class":26,"line":403},[24,5433,5434],{},"    help=\"A high-performance async load testing tool.\",\n",[24,5436,5437],{"class":26,"line":422},[24,5438,5439],{},"    no_args_is_help=True,\n",[24,5441,5442],{"class":26,"line":453},[24,5443,5444],{},")\n",[24,5446,5447],{"class":26,"line":490},[24,5448,379],{"emptyLinePlaceholder":378},[24,5450,5451],{"class":26,"line":496},[24,5452,5453],{},"console = Console()\n",[24,5455,5456],{"class":26,"line":504},[24,5457,379],{"emptyLinePlaceholder":378},[24,5459,5460],{"class":26,"line":520},[24,5461,5462],{},"@app.command()\n",[24,5464,5465],{"class":26,"line":545},[24,5466,5467],{},"def run(\n",[24,5469,5470],{"class":26,"line":571},[24,5471,5472],{},"    url: Annotated[str, typer.Argument(help=\"Target URL for load testing.\")],\n",[24,5474,5475],{"class":26,"line":640},[24,5476,5477],{},"    num_requests: Annotated[\n",[24,5479,5480],{"class":26,"line":645},[24,5481,5482],{},"        int,\n",[24,5484,5485],{"class":26,"line":5039},[24,5486,5487],{},"        typer.Option(\n",[24,5489,5490],{"class":26,"line":5045},[24,5491,5492],{},"            \"-n\", \"--requests\",\n",[24,5494,5495],{"class":26,"line":5051},[24,5496,5497],{},"            help=\"Total number of requests to send.\",\n",[24,5499,5500],{"class":26,"line":5056},[24,5501,5502],{},"            min=1,\n",[24,5504,5505],{"class":26,"line":5061},[24,5506,5507],{},"        ),\n",[24,5509,5510],{"class":26,"line":5067},[24,5511,5512],{},"    ] = 100,\n",[24,5514,5515],{"class":26,"line":5073},[24,5516,5517],{},"    verbose: Annotated[\n",[24,5519,5520],{"class":26,"line":5236},[24,5521,5522],{},"        bool,\n",[24,5524,5526],{"class":26,"line":5525},26,[24,5527,5528],{},"        typer.Option(\"-v\", \"--verbose\", help=\"Enable verbose output.\"),\n",[24,5530,5532],{"class":26,"line":5531},27,[24,5533,5534],{},"    ] = False,\n",[24,5536,5538],{"class":26,"line":5537},28,[24,5539,5540],{},") -> None:\n",[24,5542,5544],{"class":26,"line":5543},29,[24,5545,5546],{},"    \"\"\"Run a load test against a target URL.\"\"\"\n",[24,5548,5550],{"class":26,"line":5549},30,[24,5551,5552],{},"    console.print(f\"[green]Testing {url}...[\u002Fgreen]\")\n",[10,5554,5555,5556,5559],{},"Type hints become CLI arguments. Help text is generated automatically. Validation happens for free (",[21,5557,5558],{},"min=1"," ensures positive values). Rich gives me colors and formatting without any extra work.",[10,5561,5562],{},"Less code than Click or argparse, with better output.",[108,5564,5566],{"id":5565},"my-modern-python-project-structure","My Modern Python Project Structure",[10,5568,5569],{},"My current project structure:",[14,5571,5574],{"className":5572,"code":5573,"language":2908,"meta":19},[2906],"project\u002F\n├── src\u002F\n│   ├── __init__.py\n│   ├── main.py              # CLI entry point\n│   ├── models\u002F              # Pydantic models\n│   │   ├── __init__.py\n│   │   ├── config.py\n│   │   └── results.py\n│   ├── engine\u002F              # Core business logic\n│   │   ├── __init__.py\n│   │   └── runner.py\n│   └── utils\u002F\n│       └── errors.py\n├── tests\u002F\n│   ├── conftest.py          # Shared fixtures\n│   ├── unit\u002F\n│   │   └── test_models.py\n│   └── integration\u002F\n│       └── test_engine.py\n├── pyproject.toml           # Single config file\n├── Makefile                 # Common commands\n└── uv.lock                  # Lock file (auto-generated)\n",[21,5575,5573],{"__ignoreMap":19},[10,5577,5578],{},"It's feature-based, like my Vue\u002FReact projects. Each feature owns its domain. Tests mirror the source structure. Everything is discoverable.",[108,5580,5582],{"id":5581},"the-makefile-npm-scripts-for-python","The Makefile: npm Scripts for Python",[10,5584,5585],{},"I still use a Makefile for common tasks—it's the npm scripts of Python:",[14,5587,5591],{"className":5588,"code":5589,"language":5590,"meta":19,"style":19},"language-makefile shiki shiki-themes vitesse-light vitesse-dark",".PHONY: lint format typecheck test\n\nlint:\n uv run ruff check .\n\nformat:\n uv run ruff format .\n\ntypecheck:\n uv run pyright\n\ntest:\n uv run pytest tests\u002F -v\n","makefile",[21,5592,5593,5598,5602,5607,5612,5616,5621,5626,5630,5635,5640,5644,5649],{"__ignoreMap":19},[24,5594,5595],{"class":26,"line":27},[24,5596,5597],{},".PHONY: lint format typecheck test\n",[24,5599,5600],{"class":26,"line":57},[24,5601,379],{"emptyLinePlaceholder":378},[24,5603,5604],{"class":26,"line":78},[24,5605,5606],{},"lint:\n",[24,5608,5609],{"class":26,"line":226},[24,5610,5611],{}," uv run ruff check .\n",[24,5613,5614],{"class":26,"line":238},[24,5615,379],{"emptyLinePlaceholder":378},[24,5617,5618],{"class":26,"line":244},[24,5619,5620],{},"format:\n",[24,5622,5623],{"class":26,"line":403},[24,5624,5625],{}," uv run ruff format .\n",[24,5627,5628],{"class":26,"line":422},[24,5629,379],{"emptyLinePlaceholder":378},[24,5631,5632],{"class":26,"line":453},[24,5633,5634],{},"typecheck:\n",[24,5636,5637],{"class":26,"line":490},[24,5638,5639],{}," uv run pyright\n",[24,5641,5642],{"class":26,"line":496},[24,5643,379],{"emptyLinePlaceholder":378},[24,5645,5646],{"class":26,"line":504},[24,5647,5648],{},"test:\n",[24,5650,5651],{"class":26,"line":520},[24,5652,5653],{}," uv run pytest tests\u002F -v\n",[10,5655,5656,5659,5660,5662,5663,5666],{},[21,5657,5658],{},"uv run"," is the magic here. It automatically uses the project's virtual environment without me having to activate it. Just like ",[21,5661,1344],{}," or ",[21,5664,5665],{},"bunx",", but smarter.",[108,5668,5670],{"id":5669},"testing-pytest-pytest-asyncio","Testing: pytest + pytest-asyncio",[10,5672,5673],{},"Async testing with pytest-asyncio:",[14,5675,5677],{"className":5263,"code":5676,"language":4874,"meta":19,"style":19},"import pytest\nfrom src.models import LoadTestConfig, HttpMethod\n\nclass TestLoadTestConfig:\n    \"\"\"Tests for LoadTestConfig model.\"\"\"\n\n    def test_valid_config(self) -> None:\n        \"\"\"Test creating a valid load test config.\"\"\"\n        config = LoadTestConfig(\n            url=\"https:\u002F\u002Fexample.com\u002Fapi\", # type: ignore[arg-type]\n            method=HttpMethod.POST,\n            num_requests=1000,\n            concurrency=100,\n        )\n        assert config.num_requests == 1000\n\n    def test_concurrency_cannot_exceed_num_requests(self) -> None:\n        \"\"\"Test that concurrency cannot exceed num_requests.\"\"\"\n        with pytest.raises(ValidationError):\n            LoadTestConfig(\n                url=\"https:\u002F\u002Fexample.com\", # type: ignore[arg-type]\n                num_requests=10,\n                concurrency=100,\n            )\n",[21,5678,5679,5684,5689,5693,5698,5703,5707,5712,5717,5722,5727,5732,5737,5742,5747,5752,5756,5761,5766,5771,5776,5781,5786,5791],{"__ignoreMap":19},[24,5680,5681],{"class":26,"line":27},[24,5682,5683],{},"import pytest\n",[24,5685,5686],{"class":26,"line":57},[24,5687,5688],{},"from src.models import LoadTestConfig, HttpMethod\n",[24,5690,5691],{"class":26,"line":78},[24,5692,379],{"emptyLinePlaceholder":378},[24,5694,5695],{"class":26,"line":226},[24,5696,5697],{},"class TestLoadTestConfig:\n",[24,5699,5700],{"class":26,"line":238},[24,5701,5702],{},"    \"\"\"Tests for LoadTestConfig model.\"\"\"\n",[24,5704,5705],{"class":26,"line":244},[24,5706,379],{"emptyLinePlaceholder":378},[24,5708,5709],{"class":26,"line":403},[24,5710,5711],{},"    def test_valid_config(self) -> None:\n",[24,5713,5714],{"class":26,"line":422},[24,5715,5716],{},"        \"\"\"Test creating a valid load test config.\"\"\"\n",[24,5718,5719],{"class":26,"line":453},[24,5720,5721],{},"        config = LoadTestConfig(\n",[24,5723,5724],{"class":26,"line":490},[24,5725,5726],{},"            url=\"https:\u002F\u002Fexample.com\u002Fapi\", # type: ignore[arg-type]\n",[24,5728,5729],{"class":26,"line":496},[24,5730,5731],{},"            method=HttpMethod.POST,\n",[24,5733,5734],{"class":26,"line":504},[24,5735,5736],{},"            num_requests=1000,\n",[24,5738,5739],{"class":26,"line":520},[24,5740,5741],{},"            concurrency=100,\n",[24,5743,5744],{"class":26,"line":545},[24,5745,5746],{},"        )\n",[24,5748,5749],{"class":26,"line":571},[24,5750,5751],{},"        assert config.num_requests == 1000\n",[24,5753,5754],{"class":26,"line":640},[24,5755,379],{"emptyLinePlaceholder":378},[24,5757,5758],{"class":26,"line":645},[24,5759,5760],{},"    def test_concurrency_cannot_exceed_num_requests(self) -> None:\n",[24,5762,5763],{"class":26,"line":5039},[24,5764,5765],{},"        \"\"\"Test that concurrency cannot exceed num_requests.\"\"\"\n",[24,5767,5768],{"class":26,"line":5045},[24,5769,5770],{},"        with pytest.raises(ValidationError):\n",[24,5772,5773],{"class":26,"line":5051},[24,5774,5775],{},"            LoadTestConfig(\n",[24,5777,5778],{"class":26,"line":5056},[24,5779,5780],{},"                url=\"https:\u002F\u002Fexample.com\", # type: ignore[arg-type]\n",[24,5782,5783],{"class":26,"line":5061},[24,5784,5785],{},"                num_requests=10,\n",[24,5787,5788],{"class":26,"line":5067},[24,5789,5790],{},"                concurrency=100,\n",[24,5792,5793],{"class":26,"line":5073},[24,5794,5795],{},"            )\n",[10,5797,5798],{},"Pydantic validation errors tell you exactly what went wrong and where.",[108,5800,5802],{"id":5801},"what-i-actually-build-now","What I Actually Build Now",[10,5804,5805],{},"Let me show you a real async function from production:",[14,5807,5809],{"className":5263,"code":5808,"language":4874,"meta":19,"style":19},"async def run_load_test(\n    config: LoadTestConfig,\n    proxy_file: Path | None = None,\n    show_progress: bool = True,\n    output_format: str = \"human\",\n) -> Statistics:\n    \"\"\"Run a complete load test.\"\"\"\n    if output_format == \"human\":\n        print_test_header(config)\n\n    proxy_manager: ProxyManager | None = None\n    if proxy_file:\n        proxy_manager = await ProxyManager.from_file(proxy_file)\n        console.print(f\"[green]Loaded {proxy_manager.proxy_count} proxies[\u002Fgreen]\")\n\n    runner = LoadTestRunner(config=config, proxy_manager=proxy_manager)\n    stats = await runner.run(show_progress=show_progress)\n\n    if output_format == \"json\":\n        print_json_statistics(stats, config)\n    else:\n        print_statistics(stats, config)\n\n    return stats\n",[21,5810,5811,5816,5821,5826,5831,5836,5841,5846,5851,5856,5860,5865,5870,5875,5880,5884,5889,5894,5898,5903,5908,5913,5918,5922],{"__ignoreMap":19},[24,5812,5813],{"class":26,"line":27},[24,5814,5815],{},"async def run_load_test(\n",[24,5817,5818],{"class":26,"line":57},[24,5819,5820],{},"    config: LoadTestConfig,\n",[24,5822,5823],{"class":26,"line":78},[24,5824,5825],{},"    proxy_file: Path | None = None,\n",[24,5827,5828],{"class":26,"line":226},[24,5829,5830],{},"    show_progress: bool = True,\n",[24,5832,5833],{"class":26,"line":238},[24,5834,5835],{},"    output_format: str = \"human\",\n",[24,5837,5838],{"class":26,"line":244},[24,5839,5840],{},") -> Statistics:\n",[24,5842,5843],{"class":26,"line":403},[24,5844,5845],{},"    \"\"\"Run a complete load test.\"\"\"\n",[24,5847,5848],{"class":26,"line":422},[24,5849,5850],{},"    if output_format == \"human\":\n",[24,5852,5853],{"class":26,"line":453},[24,5854,5855],{},"        print_test_header(config)\n",[24,5857,5858],{"class":26,"line":490},[24,5859,379],{"emptyLinePlaceholder":378},[24,5861,5862],{"class":26,"line":496},[24,5863,5864],{},"    proxy_manager: ProxyManager | None = None\n",[24,5866,5867],{"class":26,"line":504},[24,5868,5869],{},"    if proxy_file:\n",[24,5871,5872],{"class":26,"line":520},[24,5873,5874],{},"        proxy_manager = await ProxyManager.from_file(proxy_file)\n",[24,5876,5877],{"class":26,"line":545},[24,5878,5879],{},"        console.print(f\"[green]Loaded {proxy_manager.proxy_count} proxies[\u002Fgreen]\")\n",[24,5881,5882],{"class":26,"line":571},[24,5883,379],{"emptyLinePlaceholder":378},[24,5885,5886],{"class":26,"line":640},[24,5887,5888],{},"    runner = LoadTestRunner(config=config, proxy_manager=proxy_manager)\n",[24,5890,5891],{"class":26,"line":645},[24,5892,5893],{},"    stats = await runner.run(show_progress=show_progress)\n",[24,5895,5896],{"class":26,"line":5039},[24,5897,379],{"emptyLinePlaceholder":378},[24,5899,5900],{"class":26,"line":5045},[24,5901,5902],{},"    if output_format == \"json\":\n",[24,5904,5905],{"class":26,"line":5051},[24,5906,5907],{},"        print_json_statistics(stats, config)\n",[24,5909,5910],{"class":26,"line":5056},[24,5911,5912],{},"    else:\n",[24,5914,5915],{"class":26,"line":5061},[24,5916,5917],{},"        print_statistics(stats, config)\n",[24,5919,5920],{"class":26,"line":5067},[24,5921,379],{"emptyLinePlaceholder":378},[24,5923,5924],{"class":26,"line":5073},[24,5925,5926],{},"    return stats\n",[10,5928,5929],{},"Type hints everywhere. Clean separation. Reads almost like TypeScript.",[108,5931,5933],{"id":5932},"the-confidence-boost","The Confidence Boost",[10,5935,5936],{},"Here's what strict typing and Pydantic validation give me:",[14,5938,5940],{"className":4948,"code":5939,"language":4950,"meta":19,"style":19},"[tool.mypy]\npython_version = \"3.12\"\nstrict = true\nwarn_return_any = true\nwarn_unused_ignores = true\ndisallow_untyped_defs = true\ndisallow_incomplete_defs = true\ncheck_untyped_defs = true\n",[21,5941,5942,5947,5952,5957,5962,5967,5972,5977],{"__ignoreMap":19},[24,5943,5944],{"class":26,"line":27},[24,5945,5946],{},"[tool.mypy]\n",[24,5948,5949],{"class":26,"line":57},[24,5950,5951],{},"python_version = \"3.12\"\n",[24,5953,5954],{"class":26,"line":78},[24,5955,5956],{},"strict = true\n",[24,5958,5959],{"class":26,"line":226},[24,5960,5961],{},"warn_return_any = true\n",[24,5963,5964],{"class":26,"line":238},[24,5965,5966],{},"warn_unused_ignores = true\n",[24,5968,5969],{"class":26,"line":244},[24,5970,5971],{},"disallow_untyped_defs = true\n",[24,5973,5974],{"class":26,"line":403},[24,5975,5976],{},"disallow_incomplete_defs = true\n",[24,5978,5979],{"class":26,"line":422},[24,5980,5981],{},"check_untyped_defs = true\n",[10,5983,5984,5985,5988],{},"With ",[21,5986,5987],{},"strict = true",", MyPy catches most issues. Combined with Pydantic's runtime validation, bugs fail at import time or during validation — not in production.",[108,5990,5992],{"id":5991},"the-evolution-summary","The Evolution Summary",[4057,5994,5995,6005],{},[4060,5996,5997],{},[4063,5998,5999,6002],{},[4066,6000,6001],{},"Before",[4066,6003,6004],{},"After",[4073,6006,6007,6023,6035,6043,6051,6061],{},[4063,6008,6009,6016],{},[4078,6010,6011,6013,6014],{},[21,6012,4845],{}," + ",[21,6015,5085],{},[4078,6017,6018,6020,6021],{},[21,6019,5089],{}," with ",[21,6022,5099],{},[4063,6024,6025,6031],{},[4078,6026,6027,6030],{},[21,6028,6029],{},"pip install"," + manual venv",[4078,6032,6033],{},[21,6034,5103],{},[4063,6036,6037,6040],{},[4078,6038,6039],{},"Black + Flake8 + isort",[4078,6041,6042],{},"Ruff (all-in-one, Rust-powered)",[4063,6044,6045,6048],{},[4078,6046,6047],{},"argparse \u002F Click",[4078,6049,6050],{},"Typer + Rich",[4063,6052,6053,6056],{},[4078,6054,6055],{},"Manual validation",[4078,6057,6058,6059],{},"Pydantic with ",[21,6060,5381],{},[4063,6062,6063,6069],{},[4078,6064,6065,6066],{},"Makefiles with ",[21,6067,6068],{},"source venv\u002Fbin\u002Factivate",[4078,6070,6065,6071],{},[21,6072,5658],{},[108,6074,6076],{"id":6075},"whats-next","What's Next",[10,6078,6079],{},"Modern Python with type hints, Pydantic, and uv is a different experience from the legacy tutorials. The ecosystem has matured.",[10,6081,6082,6083,6086,6087,6089],{},"Start with ",[21,6084,6085],{},"uv init",", add dependencies to ",[21,6088,5089],{},", and let Ruff handle the rest.",[1443,6091,6092],{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":19,"searchDepth":57,"depth":57,"links":6094},[6095,6096,6097,6098,6099,6100,6101,6102,6103,6104,6105,6106],{"id":4855,"depth":57,"text":4856},{"id":4935,"depth":57,"text":4936},{"id":5107,"depth":57,"text":5108},{"id":5256,"depth":57,"text":5257},{"id":5392,"depth":57,"text":5393},{"id":5565,"depth":57,"text":5566},{"id":5581,"depth":57,"text":5582},{"id":5669,"depth":57,"text":5670},{"id":5801,"depth":57,"text":5802},{"id":5932,"depth":57,"text":5933},{"id":5991,"depth":57,"text":5992},{"id":6075,"depth":57,"text":6076},"2025-12-02","How uv, Ruff, and pyproject.toml replaced my entire Python toolchain — and made it feel like TypeScript.",{},"\u002Fwritings\u002Ffrom-requirements-txt-to-pyproject-toml-python-evolution",{"title":4837,"description":6108},"writings\u002Ffrom-requirements-txt-to-pyproject-toml-python-evolution","a6tNrc4FJ6GCK_9du7gtFb0lsAoyi1-rheakrXSl6dc",{"id":6115,"title":6116,"body":6117,"category":4436,"date":7101,"description":7102,"draft":1462,"extension":1463,"meta":7103,"navigation":378,"path":7104,"seo":7105,"sitemap":378,"stem":7106,"__hash__":7107},"writings\u002Fwritings\u002Fthe-art-of-clean-feature-architecture-what-i-actually-build.md","The Art of Clean Feature Architecture: What I Actually Build",{"type":7,"value":6118,"toc":7080},[6119,6122,6126,6129,6132,6138,6141,6145,6148,6454,6457,6461,6464,6468,6541,6545,6584,6588,6658,6661,6665,6668,6819,6822,6826,6829,6832,6836,6839,6867,6871,6875,6878,6882,6885,6889,6892,6896,6899,6905,6908,6912,6915,7030,7033,7037,7040,7066,7069,7071,7074,7077],[10,6120,6121],{},"There's a disconnect between what gets debated and what gets shipped. This is the architecture I actually build — not the theoretical version, but the one that works at 2 AM.",[108,6123,6125],{"id":6124},"my-real-architecture-philosophy","My Real Architecture Philosophy",[10,6127,6128],{},"After building e-commerce platforms and SaaS dashboards, I've settled on one principle: the best architecture is obvious. Not clever. Just obvious.",[10,6130,6131],{},"Look at this structure from a recent project:",[14,6133,6136],{"className":6134,"code":6135,"language":2908,"meta":19},[2906],"src\u002F\n├── features\u002F\n│   ├── blog\u002F\n│   │   ├── components\u002F\n│   │   ├── hooks\u002F\n│   │   ├── api\u002F\n│   │   └── utils\u002F\n│   ├── chat\u002F\n│   │   ├── components\u002F\n│   │   ├── hooks\u002F\n│   │   └── utils\u002F\n│   └── products\u002F\n├── shared\u002F\n│   ├── components\u002Fui\u002F\n│   ├── hooks\u002F\n│   └── lib\u002F\n└── server\u002F\n",[21,6137,6135],{"__ignoreMap":19},[10,6139,6140],{},"Dead simple. Each feature is self-contained. Shared stuff is actually shared. Server code stays on the server. No mystery folders, no clever abstractions.",[108,6142,6144],{"id":6143},"the-component-that-changed-my-perspective","The Component That Changed My Perspective",[10,6146,6147],{},"A real component from production:",[14,6149,6153],{"className":6150,"code":6151,"language":6152,"meta":19,"style":19},"language-tsx shiki shiki-themes vitesse-light vitesse-dark","export const BlogCard = memo(function BlogCard({\n  article,\n  styles = DEFAULT_STYLES,\n}: Readonly\u003CProps>) {\n  const format = useFormatter();\n\n  const readTime = useMemo(\n    () =>\n      Math.ceil(\n        convertLexicalToPlaintext({ data: article.content }).split(\" \").length \u002F\n          200,\n      ),\n    [article.content],\n  );\n\n  return (\n    \u003CLink href={`\u002Fblog\u002F${article.slug}`}>\n      \u003CCard className=\"group h-full overflow-hidden\">\n        {\u002F*Clean, focused, single responsibility*\u002F}\n      \u003C\u002FCard>\n    \u003C\u002FLink>\n  );\n});\n","tsx",[21,6154,6155,6179,6186,6198,6217,6233,6237,6252,6260,6272,6313,6320,6325,6339,6344,6348,6356,6394,6418,6428,6437,6446,6450],{"__ignoreMap":19},[24,6156,6157,6159,6162,6165,6167,6170,6172,6175,6177],{"class":26,"line":27},[24,6158,148],{"class":30},[24,6160,6161],{"class":151}," const",[24,6163,6164],{"class":44}," BlogCard",[24,6166,158],{"class":34},[24,6168,6169],{"class":389}," memo",[24,6171,443],{"class":34},[24,6173,6174],{"class":151},"function",[24,6176,6164],{"class":389},[24,6178,392],{"class":34},[24,6180,6181,6184],{"class":26,"line":57},[24,6182,6183],{"class":44},"  article",[24,6185,4637],{"class":34},[24,6187,6188,6191,6193,6196],{"class":26,"line":78},[24,6189,6190],{"class":44},"  styles",[24,6192,158],{"class":34},[24,6194,6195],{"class":44}," DEFAULT_STYLES",[24,6197,4637],{"class":34},[24,6199,6200,6203,6206,6209,6212,6215],{"class":26,"line":226},[24,6201,6202],{"class":34},"}: ",[24,6204,6205],{"class":205},"Readonly",[24,6207,6208],{"class":34},"\u003C",[24,6210,6211],{"class":205},"Props",[24,6213,6214],{"class":34},">)",[24,6216,209],{"class":34},[24,6218,6219,6222,6225,6227,6230],{"class":26,"line":238},[24,6220,6221],{"class":151},"  const",[24,6223,6224],{"class":44}," format",[24,6226,158],{"class":34},[24,6228,6229],{"class":389}," useFormatter",[24,6231,6232],{"class":34},"();\n",[24,6234,6235],{"class":26,"line":244},[24,6236,379],{"emptyLinePlaceholder":378},[24,6238,6239,6241,6244,6246,6249],{"class":26,"line":403},[24,6240,6221],{"class":151},[24,6242,6243],{"class":44}," readTime",[24,6245,158],{"class":34},[24,6247,6248],{"class":389}," useMemo",[24,6250,6251],{"class":34},"(\n",[24,6253,6254,6257],{"class":26,"line":422},[24,6255,6256],{"class":34},"    ()",[24,6258,6259],{"class":34}," =>\n",[24,6261,6262,6265,6267,6270],{"class":26,"line":453},[24,6263,6264],{"class":44},"      Math",[24,6266,35],{"class":34},[24,6268,6269],{"class":389},"ceil",[24,6271,6251],{"class":34},[24,6273,6274,6277,6280,6283,6285,6288,6290,6293,6296,6299,6301,6303,6305,6307,6310],{"class":26,"line":490},[24,6275,6276],{"class":389},"        convertLexicalToPlaintext",[24,6278,6279],{"class":34},"({",[24,6281,6282],{"class":38}," data",[24,6284,2952],{"class":34},[24,6286,6287],{"class":44}," article",[24,6289,35],{"class":34},[24,6291,6292],{"class":44},"content",[24,6294,6295],{"class":34}," }).",[24,6297,6298],{"class":389},"split",[24,6300,443],{"class":34},[24,6302,4566],{"class":346},[24,6304,3287],{"class":346},[24,6306,2683],{"class":34},[24,6308,6309],{"class":38},"length",[24,6311,6312],{"class":151}," \u002F\n",[24,6314,6315,6318],{"class":26,"line":496},[24,6316,6317],{"class":446},"          200",[24,6319,4637],{"class":34},[24,6321,6322],{"class":26,"line":504},[24,6323,6324],{"class":34},"      ),\n",[24,6326,6327,6330,6333,6335,6337],{"class":26,"line":520},[24,6328,6329],{"class":34},"    [",[24,6331,6332],{"class":44},"article",[24,6334,35],{"class":34},[24,6336,6292],{"class":44},[24,6338,1208],{"class":34},[24,6340,6341],{"class":26,"line":545},[24,6342,6343],{"class":34},"  );\n",[24,6345,6346],{"class":26,"line":571},[24,6347,379],{"emptyLinePlaceholder":378},[24,6349,6350,6353],{"class":26,"line":640},[24,6351,6352],{"class":30},"  return",[24,6354,6355],{"class":34}," (\n",[24,6357,6358,6361,6364,6367,6370,6373,6376,6379,6381,6383,6386,6389,6391],{"class":26,"line":645},[24,6359,6360],{"class":34},"    \u003C",[24,6362,6363],{"class":38},"Link",[24,6365,6366],{"class":44}," href",[24,6368,6369],{"class":34},"={",[24,6371,6372],{"class":346},"`",[24,6374,6375],{"class":350},"\u002Fblog\u002F",[24,6377,6378],{"class":30},"${",[24,6380,6332],{"class":350},[24,6382,35],{"class":34},[24,6384,6385],{"class":350},"slug",[24,6387,6388],{"class":30},"}",[24,6390,6372],{"class":346},[24,6392,6393],{"class":34},"}>\n",[24,6395,6396,6399,6402,6405,6408,6410,6413,6415],{"class":26,"line":5039},[24,6397,6398],{"class":34},"      \u003C",[24,6400,6401],{"class":38},"Card",[24,6403,6404],{"class":44}," className",[24,6406,6407],{"class":34},"=",[24,6409,4566],{"class":346},[24,6411,6412],{"class":350},"group h-full overflow-hidden",[24,6414,4566],{"class":346},[24,6416,6417],{"class":34},">\n",[24,6419,6420,6423,6426],{"class":26,"line":5045},[24,6421,6422],{"class":34},"        {",[24,6424,6425],{"class":53},"\u002F*Clean, focused, single responsibility*\u002F",[24,6427,247],{"class":34},[24,6429,6430,6433,6435],{"class":26,"line":5051},[24,6431,6432],{"class":34},"      \u003C\u002F",[24,6434,6401],{"class":38},[24,6436,6417],{"class":34},[24,6438,6439,6442,6444],{"class":26,"line":5056},[24,6440,6441],{"class":34},"    \u003C\u002F",[24,6443,6363],{"class":38},[24,6445,6417],{"class":34},[24,6447,6448],{"class":26,"line":5061},[24,6449,6343],{"class":34},[24,6451,6452],{"class":26,"line":5067},[24,6453,4653],{"class":34},[10,6455,6456],{},"No prop drilling. No context spaghetti. No clever abstractions. One component, one job.",[108,6458,6460],{"id":6459},"the-pattern-i-keep-coming-back-to","The Pattern I Keep Coming Back To",[10,6462,6463],{},"The pattern I use:",[2795,6465,6467],{"id":6466},"_1-features-own-their-domain","1. Features Own Their Domain",[14,6469,6471],{"className":4543,"code":6470,"language":4545,"meta":19,"style":19},"\u002F\u002F features\u002Fchat\u002Fhooks\u002FuseChatApi.ts\nexport function useChatApi() {\n  \u002F\u002F All chat logic lives here\n  \u002F\u002F Not scattered across utils, helpers, services\n}\n\n\u002F\u002F features\u002Fproducts\u002Fapi\u002Findex.ts\nexport async function getProducts() {\n  \u002F\u002F Product API calls stay with products\n}\n",[21,6472,6473,6478,6493,6498,6503,6507,6511,6516,6532,6537],{"__ignoreMap":19},[24,6474,6475],{"class":26,"line":27},[24,6476,6477],{"class":53},"\u002F\u002F features\u002Fchat\u002Fhooks\u002FuseChatApi.ts\n",[24,6479,6480,6482,6485,6488,6491],{"class":26,"line":57},[24,6481,148],{"class":30},[24,6483,6484],{"class":151}," function",[24,6486,6487],{"class":389}," useChatApi",[24,6489,6490],{"class":34},"()",[24,6492,209],{"class":34},[24,6494,6495],{"class":26,"line":78},[24,6496,6497],{"class":53},"  \u002F\u002F All chat logic lives here\n",[24,6499,6500],{"class":26,"line":226},[24,6501,6502],{"class":53},"  \u002F\u002F Not scattered across utils, helpers, services\n",[24,6504,6505],{"class":26,"line":238},[24,6506,247],{"class":34},[24,6508,6509],{"class":26,"line":244},[24,6510,379],{"emptyLinePlaceholder":378},[24,6512,6513],{"class":26,"line":403},[24,6514,6515],{"class":53},"\u002F\u002F features\u002Fproducts\u002Fapi\u002Findex.ts\n",[24,6517,6518,6520,6523,6525,6528,6530],{"class":26,"line":422},[24,6519,148],{"class":30},[24,6521,6522],{"class":151}," async",[24,6524,6484],{"class":151},[24,6526,6527],{"class":389}," getProducts",[24,6529,6490],{"class":34},[24,6531,209],{"class":34},[24,6533,6534],{"class":26,"line":453},[24,6535,6536],{"class":53},"  \u002F\u002F Product API calls stay with products\n",[24,6538,6539],{"class":26,"line":490},[24,6540,247],{"class":34},[2795,6542,6544],{"id":6543},"_2-shared-means-actually-shared","2. Shared Means Actually Shared",[14,6546,6548],{"className":6150,"code":6547,"language":6152,"meta":19,"style":19},"\u002F\u002F shared\u002Fcomponents\u002Fui\u002Fbutton.tsx\n\u002F\u002F This button is used EVERYWHERE\n\u002F\u002F Not \"might be shared someday\"\n\n\u002F\u002F shared\u002Fhooks\u002FuseCopy.ts\n\u002F\u002F A hook that 5+ features actually use\n\u002F\u002F Not a \"just in case\" abstraction\n",[21,6549,6550,6555,6560,6565,6569,6574,6579],{"__ignoreMap":19},[24,6551,6552],{"class":26,"line":27},[24,6553,6554],{"class":53},"\u002F\u002F shared\u002Fcomponents\u002Fui\u002Fbutton.tsx\n",[24,6556,6557],{"class":26,"line":57},[24,6558,6559],{"class":53},"\u002F\u002F This button is used EVERYWHERE\n",[24,6561,6562],{"class":26,"line":78},[24,6563,6564],{"class":53},"\u002F\u002F Not \"might be shared someday\"\n",[24,6566,6567],{"class":26,"line":226},[24,6568,379],{"emptyLinePlaceholder":378},[24,6570,6571],{"class":26,"line":238},[24,6572,6573],{"class":53},"\u002F\u002F shared\u002Fhooks\u002FuseCopy.ts\n",[24,6575,6576],{"class":26,"line":244},[24,6577,6578],{"class":53},"\u002F\u002F A hook that 5+ features actually use\n",[24,6580,6581],{"class":26,"line":403},[24,6582,6583],{"class":53},"\u002F\u002F Not a \"just in case\" abstraction\n",[2795,6585,6587],{"id":6586},"_3-clean-imports-tell-the-story","3. Clean Imports Tell the Story",[14,6589,6591],{"className":6150,"code":6590,"language":6152,"meta":19,"style":19},"import { BlogCard } from \"@\u002Ffeatures\u002Fblog\u002Fcomponents\u002Fcard\";\nimport { Button } from \"@\u002Fshared\u002Fcomponents\u002Fui\u002Fbutton\";\nimport { api } from \"@\u002Fserver\u002Ftrpc\";\n",[21,6592,6593,6614,6636],{"__ignoreMap":19},[24,6594,6595,6597,6599,6601,6603,6605,6607,6610,6612],{"class":26,"line":27},[24,6596,31],{"class":30},[24,6598,334],{"class":34},[24,6600,6164],{"class":44},[24,6602,340],{"class":34},[24,6604,343],{"class":30},[24,6606,3287],{"class":346},[24,6608,6609],{"class":350},"@\u002Ffeatures\u002Fblog\u002Fcomponents\u002Fcard",[24,6611,4566],{"class":346},[24,6613,4569],{"class":34},[24,6615,6616,6618,6620,6623,6625,6627,6629,6632,6634],{"class":26,"line":57},[24,6617,31],{"class":30},[24,6619,334],{"class":34},[24,6621,6622],{"class":44}," Button",[24,6624,340],{"class":34},[24,6626,343],{"class":30},[24,6628,3287],{"class":346},[24,6630,6631],{"class":350},"@\u002Fshared\u002Fcomponents\u002Fui\u002Fbutton",[24,6633,4566],{"class":346},[24,6635,4569],{"class":34},[24,6637,6638,6640,6642,6645,6647,6649,6651,6654,6656],{"class":26,"line":78},[24,6639,31],{"class":30},[24,6641,334],{"class":34},[24,6643,6644],{"class":44}," api",[24,6646,340],{"class":34},[24,6648,343],{"class":30},[24,6650,3287],{"class":346},[24,6652,6653],{"class":350},"@\u002Fserver\u002Ftrpc",[24,6655,4566],{"class":346},[24,6657,4569],{"class":34},[10,6659,6660],{},"One glance and you know exactly where everything comes from. No detective work required.",[108,6662,6664],{"id":6663},"the-hero-component-philosophy","The Hero Component Philosophy",[10,6666,6667],{},"Every feature gets a hero section. Same pattern every time:",[14,6669,6671],{"className":6150,"code":6670,"language":6152,"meta":19,"style":19},"export const HeroSection = memo(() => {\n  const t = useTranslations(\"pages.home.hero\");\n  const [state, setState] = useState();\n\n  \u002F\u002F Effects close to usage\n  \u002F\u002F No effect chains\n  \u002F\u002F No callback hell\n\n  return (\n    \u003Csection className=\"relative flex h-dvh items-center\">\n      {\u002F*Content*\u002F}\n    \u003C\u002Fsection>\n  );\n});\n",[21,6672,6673,6694,6718,6744,6748,6753,6758,6763,6767,6773,6793,6803,6811,6815],{"__ignoreMap":19},[24,6674,6675,6677,6679,6682,6684,6686,6689,6692],{"class":26,"line":27},[24,6676,148],{"class":30},[24,6678,6161],{"class":151},[24,6680,6681],{"class":44}," HeroSection",[24,6683,158],{"class":34},[24,6685,6169],{"class":389},[24,6687,6688],{"class":34},"(()",[24,6690,6691],{"class":34}," =>",[24,6693,209],{"class":34},[24,6695,6696,6698,6701,6703,6706,6708,6710,6713,6715],{"class":26,"line":57},[24,6697,6221],{"class":151},[24,6699,6700],{"class":44}," t",[24,6702,158],{"class":34},[24,6704,6705],{"class":389}," useTranslations",[24,6707,443],{"class":34},[24,6709,4566],{"class":346},[24,6711,6712],{"class":350},"pages.home.hero",[24,6714,4566],{"class":346},[24,6716,6717],{"class":34},");\n",[24,6719,6720,6722,6725,6728,6731,6734,6737,6739,6742],{"class":26,"line":78},[24,6721,6221],{"class":151},[24,6723,6724],{"class":34}," [",[24,6726,6727],{"class":44},"state",[24,6729,6730],{"class":34},",",[24,6732,6733],{"class":44}," setState",[24,6735,6736],{"class":34},"]",[24,6738,158],{"class":34},[24,6740,6741],{"class":389}," useState",[24,6743,6232],{"class":34},[24,6745,6746],{"class":26,"line":226},[24,6747,379],{"emptyLinePlaceholder":378},[24,6749,6750],{"class":26,"line":238},[24,6751,6752],{"class":53},"  \u002F\u002F Effects close to usage\n",[24,6754,6755],{"class":26,"line":244},[24,6756,6757],{"class":53},"  \u002F\u002F No effect chains\n",[24,6759,6760],{"class":26,"line":403},[24,6761,6762],{"class":53},"  \u002F\u002F No callback hell\n",[24,6764,6765],{"class":26,"line":422},[24,6766,379],{"emptyLinePlaceholder":378},[24,6768,6769,6771],{"class":26,"line":453},[24,6770,6352],{"class":30},[24,6772,6355],{"class":34},[24,6774,6775,6777,6780,6782,6784,6786,6789,6791],{"class":26,"line":490},[24,6776,6360],{"class":34},[24,6778,6779],{"class":30},"section",[24,6781,6404],{"class":44},[24,6783,6407],{"class":34},[24,6785,4566],{"class":346},[24,6787,6788],{"class":350},"relative flex h-dvh items-center",[24,6790,4566],{"class":346},[24,6792,6417],{"class":34},[24,6794,6795,6798,6801],{"class":26,"line":496},[24,6796,6797],{"class":34},"      {",[24,6799,6800],{"class":53},"\u002F*Content*\u002F",[24,6802,247],{"class":34},[24,6804,6805,6807,6809],{"class":26,"line":504},[24,6806,6441],{"class":34},[24,6808,6779],{"class":30},[24,6810,6417],{"class":34},[24,6812,6813],{"class":26,"line":520},[24,6814,6343],{"class":34},[24,6816,6817],{"class":26,"line":545},[24,6818,4653],{"class":34},[10,6820,6821],{},"Centered. Focused. No distractions. The architecture mirrors the UI.",[108,6823,6825],{"id":6824},"why-i-stopped-chasing-perfect","Why I Stopped Chasing Perfect",[10,6827,6828],{},"I tried Domain-Driven Design, Clean Architecture, Hexagonal Architecture. The best one turned out to be whichever your team understands instantly.",[10,6830,6831],{},"New developers understand this approach in minutes. Features ship faster. Bugs are easier to find.",[108,6833,6835],{"id":6834},"the-real-world-test","The Real-World Test",[10,6837,6838],{},"My test for whether an architecture works:",[6840,6841,6842,6851,6861],"ol",{},[1405,6843,6844,6847,6848,35],{},[265,6845,6846],{},"Can you find the bug at 3 AM?"," With feature folders, yes. The error is in the checkout feature? Check ",[21,6849,6850],{},"features\u002Fcheckout",[1405,6852,6853,6856,6857,6860],{},[265,6854,6855],{},"Can a junior dev add a feature?"," Create ",[21,6858,6859],{},"features\u002Fnew-thing",", follow the pattern from other features. Done.",[1405,6862,6863,6866],{},[265,6864,6865],{},"Can you delete a feature cleanly?"," Delete the folder. If anything breaks, it wasn't properly isolated.",[108,6868,6870],{"id":6869},"the-mistakes-that-led-me-here","The Mistakes That Led Me Here",[2795,6872,6874],{"id":6873},"the-monorepo-phase","The Monorepo Phase",[10,6876,6877],{},"I went through a phase where everything had to be a monorepo with 47 packages. Took 5 minutes just to understand the import paths. Now? One codebase, clear boundaries.",[2795,6879,6881],{"id":6880},"the-abstraction-addiction","The Abstraction Addiction",[10,6883,6884],{},"I once created a \"FormBuilder\" that could handle any form. It had 2000 lines of configuration options. Now I just write forms. Takes 10 minutes, works every time.",[2795,6886,6888],{"id":6887},"the-perfect-type-system","The Perfect Type System",[10,6890,6891],{},"Spent weeks on a type system that covered every edge case. Nobody understood it. Now I type what matters and move on.",[108,6893,6895],{"id":6894},"what-this-actually-looks-like-in-production","What This Actually Looks Like in Production",[10,6897,6898],{},"Here's a real feature structure from a production app:",[14,6900,6903],{"className":6901,"code":6902,"language":2908,"meta":19},[2906],"features\u002Fverification\u002F\n├── components\u002F\n│   ├── hero-section.tsx        # The main hero\n│   ├── verification-form.tsx   # The form\n│   └── results\u002F                # Result states\n│       ├── success.tsx\n│       └── error.tsx\n├── api\u002F\n│   └── index.ts                # API calls\n├── types\u002F\n│   └── index.ts                # Types\n└── utils\u002F\n    └── validation.ts           # Validation logic\n",[21,6904,6902],{"__ignoreMap":19},[10,6906,6907],{},"Everything the verification feature needs is right there. No hunting, no guessing, no \"where did they put this?\"",[108,6909,6911],{"id":6910},"the-tooling-that-makes-it-work","The Tooling That Makes It Work",[10,6913,6914],{},"Architecture isn't just folders. It's the entire developer experience:",[14,6916,6920],{"className":6917,"code":6918,"language":6919,"meta":19,"style":19},"language-json shiki shiki-themes vitesse-light vitesse-dark","{\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"lint\": \"eslint .\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n","json",[21,6921,6922,6927,6942,6963,6983,7003,7021,7026],{"__ignoreMap":19},[24,6923,6924],{"class":26,"line":27},[24,6925,6926],{"class":34},"{\n",[24,6928,6929,6933,6936,6938,6940],{"class":26,"line":57},[24,6930,6932],{"class":6931},"sRmUx","  \"",[24,6934,6935],{"class":38},"scripts",[24,6937,4566],{"class":6931},[24,6939,2952],{"class":34},[24,6941,209],{"class":34},[24,6943,6944,6947,6950,6952,6954,6956,6959,6961],{"class":26,"line":78},[24,6945,6946],{"class":6931},"    \"",[24,6948,6949],{"class":38},"dev",[24,6951,4566],{"class":6931},[24,6953,2952],{"class":34},[24,6955,3287],{"class":346},[24,6957,6958],{"class":350},"next dev",[24,6960,4566],{"class":346},[24,6962,4637],{"class":34},[24,6964,6965,6967,6970,6972,6974,6976,6979,6981],{"class":26,"line":226},[24,6966,6946],{"class":6931},[24,6968,6969],{"class":38},"build",[24,6971,4566],{"class":6931},[24,6973,2952],{"class":34},[24,6975,3287],{"class":346},[24,6977,6978],{"class":350},"next build",[24,6980,4566],{"class":346},[24,6982,4637],{"class":34},[24,6984,6985,6987,6990,6992,6994,6996,6999,7001],{"class":26,"line":238},[24,6986,6946],{"class":6931},[24,6988,6989],{"class":38},"lint",[24,6991,4566],{"class":6931},[24,6993,2952],{"class":34},[24,6995,3287],{"class":346},[24,6997,6998],{"class":350},"eslint .",[24,7000,4566],{"class":346},[24,7002,4637],{"class":34},[24,7004,7005,7007,7010,7012,7014,7016,7019],{"class":26,"line":244},[24,7006,6946],{"class":6931},[24,7008,7009],{"class":38},"typecheck",[24,7011,4566],{"class":6931},[24,7013,2952],{"class":34},[24,7015,3287],{"class":346},[24,7017,7018],{"class":350},"tsc --noEmit",[24,7020,3293],{"class":346},[24,7022,7023],{"class":26,"line":403},[24,7024,7025],{"class":34},"  }\n",[24,7027,7028],{"class":26,"line":422},[24,7029,247],{"class":34},[10,7031,7032],{},"Simple scripts. No custom build tools. No proprietary abstractions. Just the tools everyone knows.",[108,7034,7036],{"id":7035},"the-payoff","The Payoff",[10,7038,7039],{},"Six months into using this architecture on multiple projects:",[1402,7041,7042,7048,7054,7060],{},[1405,7043,7044,7047],{},[265,7045,7046],{},"Onboarding time:"," 1 day instead of 1 week",[1405,7049,7050,7053],{},[265,7051,7052],{},"Feature development:"," 40% faster (measured, not guessed)",[1405,7055,7056,7059],{},[265,7057,7058],{},"Bug resolution:"," Usually under an hour",[1405,7061,7062,7065],{},[265,7063,7064],{},"Developer satisfaction:"," noticeably better",[10,7067,7068],{},"The real payoff: I stopped thinking about architecture. It gets out of the way.",[108,7070,4401],{"id":4400},[10,7072,7073],{},"Good architecture is invisible. It does its job and gets out of the way.",[10,7075,7076],{},"It ships. It scales. It makes sense. That's enough.",[1443,7078,7079],{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sRmUx, html code.shiki .sRmUx{--shiki-default:#99841877;--shiki-dark:#B8A96577}",{"title":19,"searchDepth":57,"depth":57,"links":7081},[7082,7083,7084,7089,7090,7091,7092,7097,7098,7099,7100],{"id":6124,"depth":57,"text":6125},{"id":6143,"depth":57,"text":6144},{"id":6459,"depth":57,"text":6460,"children":7085},[7086,7087,7088],{"id":6466,"depth":78,"text":6467},{"id":6543,"depth":78,"text":6544},{"id":6586,"depth":78,"text":6587},{"id":6663,"depth":57,"text":6664},{"id":6824,"depth":57,"text":6825},{"id":6834,"depth":57,"text":6835},{"id":6869,"depth":57,"text":6870,"children":7093},[7094,7095,7096],{"id":6873,"depth":78,"text":6874},{"id":6880,"depth":78,"text":6881},{"id":6887,"depth":78,"text":6888},{"id":6894,"depth":57,"text":6895},{"id":6910,"depth":57,"text":6911},{"id":7035,"depth":57,"text":7036},{"id":4400,"depth":57,"text":4401},"2025-08-16","Feature-based folders, minimal abstractions, obvious boundaries. The architecture I actually ship.",{},"\u002Fwritings\u002Fthe-art-of-clean-feature-architecture-what-i-actually-build",{"title":6116,"description":7102},"writings\u002Fthe-art-of-clean-feature-architecture-what-i-actually-build","shAP5heibWK9EJ_tfngyXskISlpPsuQAP155RMn4QSM",{"id":7109,"title":7110,"body":7111,"category":4436,"date":7587,"description":7588,"draft":1462,"extension":1463,"meta":7589,"navigation":378,"path":7590,"seo":7591,"sitemap":378,"stem":7592,"__hash__":7593},"writings\u002Fwritings\u002Fa-node-js-developers-journey-into-python-territory.md","A Node.js Developer's Journey into Python Territory",{"type":7,"value":7112,"toc":7574},[7113,7116,7120,7126,7155,7158,7176,7179,7183,7186,7190,7193,7217,7220,7224,7227,7282,7286,7289,7309,7312,7316,7319,7341,7344,7348,7351,7428,7431,7435,7438,7472,7475,7479,7482,7499,7563,7565,7568,7571],[10,7114,7115],{},"My first serious Python project as a Node.js developer was disorienting. The package management, the project structure, the development workflow — everything worked differently. Some of it was better. Some of it was frustrating. All of it was worth understanding.",[108,7117,7119],{"id":7118},"the-cultural-shock-package-management","The Cultural Shock: Package Management",[10,7121,7122,7123,7125],{},"Coming from npm's ",[21,7124,5093],{}," and node_modules, Python's approach to dependencies took some adjustment.",[14,7127,7129],{"className":5263,"code":7128,"language":4874,"meta":19,"style":19},"# requirements.txt - Python's answer to package.json dependencies\n\nflask==3.1.0\nrequests==2.32.3\nSQLAlchemy==2.0.40\n",[21,7130,7131,7136,7140,7145,7150],{"__ignoreMap":19},[24,7132,7133],{"class":26,"line":27},[24,7134,7135],{},"# requirements.txt - Python's answer to package.json dependencies\n",[24,7137,7138],{"class":26,"line":57},[24,7139,379],{"emptyLinePlaceholder":378},[24,7141,7142],{"class":26,"line":78},[24,7143,7144],{},"flask==3.1.0\n",[24,7146,7147],{"class":26,"line":226},[24,7148,7149],{},"requests==2.32.3\n",[24,7151,7152],{"class":26,"line":238},[24,7153,7154],{},"SQLAlchemy==2.0.40\n",[10,7156,7157],{},"What struck me immediately was the absence of a central configuration file that handled both dependencies and scripts. Instead, Python projects typically separate these concerns:",[1402,7159,7160,7165,7170],{},[1405,7161,7162,7164],{},[21,7163,4845],{}," for production dependencies",[1405,7166,7167,7169],{},[21,7168,5085],{}," for development tools",[1405,7171,7172,7175],{},[21,7173,7174],{},"Makefile"," or scripts for automation",[10,7177,7178],{},"This separation offers flexibility but requires a mental shift when you're used to having everything defined in a single manifest file.",[108,7180,7182],{"id":7181},"the-python-development-trinity-black-flake8-and-mypy","The Python Development Trinity: Black, Flake8, and MyPy",[10,7184,7185],{},"If ESLint and Prettier are the guardians of JavaScript code quality, Python has its own set of defenders:",[2795,7187,7189],{"id":7188},"black-the-uncompromising-formatter","Black: The Uncompromising Formatter",[10,7191,7192],{},"Black is Python's Prettier — it formats your code with minimal configuration options. That turned out to be exactly what I needed.",[14,7194,7196],{"className":1330,"code":7195,"language":1332,"meta":19,"style":19},"# Adding to requirements-dev.txt\n\nblack==25.1.0\n",[21,7197,7198,7203,7207],{"__ignoreMap":19},[24,7199,7200],{"class":26,"line":27},[24,7201,7202],{"class":53},"# Adding to requirements-dev.txt\n",[24,7204,7205],{"class":26,"line":57},[24,7206,379],{"emptyLinePlaceholder":378},[24,7208,7209,7212,7214],{"class":26,"line":78},[24,7210,7211],{"class":44},"black",[24,7213,6407],{"class":34},[24,7215,7216],{"class":350},"=25.1.0\n",[10,7218,7219],{},"No more style debates. Black just works.",[2795,7221,7223],{"id":7222},"flake8-the-linter","Flake8: The Linter",[10,7225,7226],{},"Flake8 combines multiple Python linting tools into one package. It identifies potential bugs, enforces style guides, and checks for code complexity.",[14,7228,7230],{"className":1330,"code":7229,"language":1332,"meta":19,"style":19},"# A typical .flake8 configuration\n\n[flake8]\nmax-line-length = 88\nextend-ignore = E203\nexclude = .git,__pycache__,build,dist\n",[21,7231,7232,7237,7241,7252,7262,7272],{"__ignoreMap":19},[24,7233,7234],{"class":26,"line":27},[24,7235,7236],{"class":53},"# A typical .flake8 configuration\n",[24,7238,7239],{"class":26,"line":57},[24,7240,379],{"emptyLinePlaceholder":378},[24,7242,7243,7246,7250],{"class":26,"line":78},[24,7244,7245],{"class":34},"[",[24,7247,7249],{"class":7248},"s8w-G","flake8",[24,7251,5007],{"class":34},[24,7253,7254,7257,7259],{"class":26,"line":226},[24,7255,7256],{"class":389},"max-line-length",[24,7258,158],{"class":350},[24,7260,7261],{"class":446}," 88\n",[24,7263,7264,7267,7269],{"class":26,"line":238},[24,7265,7266],{"class":389},"extend-ignore",[24,7268,158],{"class":350},[24,7270,7271],{"class":350}," E203\n",[24,7273,7274,7277,7279],{"class":26,"line":244},[24,7275,7276],{"class":389},"exclude",[24,7278,158],{"class":350},[24,7280,7281],{"class":350}," .git,__pycache__,build,dist\n",[2795,7283,7285],{"id":7284},"mypy-type-checking-without-typescript","MyPy: Type Checking Without TypeScript",[10,7287,7288],{},"MyPy is Python's static type checker. Coming from TypeScript, this was the tool I was most relieved to find:",[14,7290,7292],{"className":5263,"code":7291,"language":4874,"meta":19,"style":19},"def get_user(user_id: int) -> dict:\n    \"\"\"Retrieve user data from the database.\"\"\"\n    return {\"id\": user_id, \"name\": \"John Doe\", \"active\": True}\n",[21,7293,7294,7299,7304],{"__ignoreMap":19},[24,7295,7296],{"class":26,"line":27},[24,7297,7298],{},"def get_user(user_id: int) -> dict:\n",[24,7300,7301],{"class":26,"line":57},[24,7302,7303],{},"    \"\"\"Retrieve user data from the database.\"\"\"\n",[24,7305,7306],{"class":26,"line":78},[24,7307,7308],{},"    return {\"id\": user_id, \"name\": \"John Doe\", \"active\": True}\n",[10,7310,7311],{},"MyPy catches type-related bugs before runtime — familiar territory for TypeScript developers.",[108,7313,7315],{"id":7314},"the-unsung-hero-vulture","The Unsung Hero: Vulture",[10,7317,7318],{},"One tool I didn't initially appreciate was Vulture – a utility that finds unused code. In large JavaScript projects, tree-shaking often handles this automatically, but Python's dynamic nature makes dead code detection valuable:",[14,7320,7322],{"className":1330,"code":7321,"language":1332,"meta":19,"style":19},"# Find unused code\n\nvulture my_project\u002F\n",[21,7323,7324,7329,7333],{"__ignoreMap":19},[24,7325,7326],{"class":26,"line":27},[24,7327,7328],{"class":53},"# Find unused code\n",[24,7330,7331],{"class":26,"line":57},[24,7332,379],{"emptyLinePlaceholder":378},[24,7334,7335,7338],{"class":26,"line":78},[24,7336,7337],{"class":389},"vulture",[24,7339,7340],{"class":350}," my_project\u002F\n",[10,7342,7343],{},"Vulture has caught abandoned functions I would have maintained indefinitely.",[108,7345,7347],{"id":7346},"automation-the-makefile-renaissance","Automation: The Makefile Renaissance",[10,7349,7350],{},"In JavaScript projects, npm scripts handle most automation tasks. Python developers, however, often reach for a much older tool: Make.",[14,7352,7354],{"className":5588,"code":7353,"language":5590,"meta":19,"style":19},"# Makefile\n\n.PHONY: format lint test clean\n\nformat:\n black src tests\n\nlint:\n flake8 src tests\n mypy src\n\ntest:\n pytest tests\u002F\n\nclean:\n rm -rf __pycache__\u002F .pytest_cache\u002F .mypy_cache\u002F\n",[21,7355,7356,7361,7365,7370,7374,7378,7383,7387,7391,7396,7401,7405,7409,7414,7418,7423],{"__ignoreMap":19},[24,7357,7358],{"class":26,"line":27},[24,7359,7360],{},"# Makefile\n",[24,7362,7363],{"class":26,"line":57},[24,7364,379],{"emptyLinePlaceholder":378},[24,7366,7367],{"class":26,"line":78},[24,7368,7369],{},".PHONY: format lint test clean\n",[24,7371,7372],{"class":26,"line":226},[24,7373,379],{"emptyLinePlaceholder":378},[24,7375,7376],{"class":26,"line":238},[24,7377,5620],{},[24,7379,7380],{"class":26,"line":244},[24,7381,7382],{}," black src tests\n",[24,7384,7385],{"class":26,"line":403},[24,7386,379],{"emptyLinePlaceholder":378},[24,7388,7389],{"class":26,"line":422},[24,7390,5606],{},[24,7392,7393],{"class":26,"line":453},[24,7394,7395],{}," flake8 src tests\n",[24,7397,7398],{"class":26,"line":490},[24,7399,7400],{}," mypy src\n",[24,7402,7403],{"class":26,"line":496},[24,7404,379],{"emptyLinePlaceholder":378},[24,7406,7407],{"class":26,"line":504},[24,7408,5648],{},[24,7410,7411],{"class":26,"line":520},[24,7412,7413],{}," pytest tests\u002F\n",[24,7415,7416],{"class":26,"line":545},[24,7417,379],{"emptyLinePlaceholder":378},[24,7419,7420],{"class":26,"line":571},[24,7421,7422],{},"clean:\n",[24,7424,7425],{"class":26,"line":640},[24,7426,7427],{}," rm -rf __pycache__\u002F .pytest_cache\u002F .mypy_cache\u002F\n",[10,7429,7430],{},"A Makefile is a simple, language-agnostic command registry — useful when projects mix Python with other technologies.",[108,7432,7434],{"id":7433},"virtual-environments-the-nodejs-devs-confusion","Virtual Environments: The Node.js Dev's Confusion",[10,7436,7437],{},"The concept that took longest to appreciate was virtual environments. In Node.js, dependencies are project-scoped by default. Python requires explicit isolation:",[14,7439,7441],{"className":1330,"code":7440,"language":1332,"meta":19,"style":19},"# Creating and activating a virtual environment\n\npython -m venv venv\nsource venv\u002Fbin\u002Factivate  # On Windows: venv\\Scripts\\activate\n",[21,7442,7443,7448,7452,7462],{"__ignoreMap":19},[24,7444,7445],{"class":26,"line":27},[24,7446,7447],{"class":53},"# Creating and activating a virtual environment\n",[24,7449,7450],{"class":26,"line":57},[24,7451,379],{"emptyLinePlaceholder":378},[24,7453,7454,7456,7458,7460],{"class":26,"line":78},[24,7455,4874],{"class":389},[24,7457,3493],{"class":3231},[24,7459,4879],{"class":350},[24,7461,4882],{"class":350},[24,7463,7464,7466,7469],{"class":26,"line":226},[24,7465,4887],{"class":38},[24,7467,7468],{"class":350}," venv\u002Fbin\u002Factivate",[24,7470,7471],{"class":53},"  # On Windows: venv\\Scripts\\activate\n",[10,7473,7474],{},"It felt like extra overhead until I hit dependency conflicts between projects. Now I see virtual environments as a more transparent version of what npm does behind the scenes.",[108,7476,7478],{"id":7477},"bringing-it-all-together-my-python-development-workflow","Bringing It All Together: My Python Development Workflow",[10,7480,7481],{},"After months of exploration, I settled on a workflow that feels natural for a Node.js developer working in Python:",[6840,7483,7484,7487,7490,7493,7496],{},[1405,7485,7486],{},"Set up a virtual environment for each project",[1405,7488,7489],{},"Create separate requirements files for production and development",[1405,7491,7492],{},"Configure Black, Flake8, and MyPy for code quality",[1405,7494,7495],{},"Use a Makefile for common tasks",[1405,7497,7498],{},"Set up pre-commit hooks for automated checks",[14,7500,7502],{"className":1330,"code":7501,"language":1332,"meta":19,"style":19},"# A typical development setup\n\npython -m venv venv\nsource venv\u002Fbin\u002Factivate\npip install -r requirements.txt -r requirements-dev.txt\nmake format  # Run Black\nmake lint    # Run Flake8 and MyPy\n",[21,7503,7504,7509,7513,7523,7529,7543,7553],{"__ignoreMap":19},[24,7505,7506],{"class":26,"line":27},[24,7507,7508],{"class":53},"# A typical development setup\n",[24,7510,7511],{"class":26,"line":57},[24,7512,379],{"emptyLinePlaceholder":378},[24,7514,7515,7517,7519,7521],{"class":26,"line":78},[24,7516,4874],{"class":389},[24,7518,3493],{"class":3231},[24,7520,4879],{"class":350},[24,7522,4882],{"class":350},[24,7524,7525,7527],{"class":26,"line":226},[24,7526,4887],{"class":38},[24,7528,4890],{"class":350},[24,7530,7531,7533,7535,7537,7539,7541],{"class":26,"line":238},[24,7532,4895],{"class":389},[24,7534,3209],{"class":350},[24,7536,4900],{"class":3231},[24,7538,4903],{"class":350},[24,7540,4900],{"class":3231},[24,7542,4908],{"class":350},[24,7544,7545,7548,7550],{"class":26,"line":244},[24,7546,7547],{"class":389},"make",[24,7549,6224],{"class":350},[24,7551,7552],{"class":53},"  # Run Black\n",[24,7554,7555,7557,7560],{"class":26,"line":403},[24,7556,7547],{"class":389},[24,7558,7559],{"class":350}," lint",[24,7561,7562],{"class":53},"    # Run Flake8 and MyPy\n",[108,7564,3127],{"id":3126},[10,7566,7567],{},"The biggest lesson from crossing ecosystems: work with the language's approach instead of fighting it. Python's philosophy — explicit over implicit, one obvious way to do things — is different from Node.js, but the goal is the same: clean, maintainable code that solves real problems.",[10,7569,7570],{},"I initially tried to make Python feel like Node.js. The setup got better once I stopped.",[1443,7572,7573],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}",{"title":19,"searchDepth":57,"depth":57,"links":7575},[7576,7577,7582,7583,7584,7585,7586],{"id":7118,"depth":57,"text":7119},{"id":7181,"depth":57,"text":7182,"children":7578},[7579,7580,7581],{"id":7188,"depth":78,"text":7189},{"id":7222,"depth":78,"text":7223},{"id":7284,"depth":78,"text":7285},{"id":7314,"depth":57,"text":7315},{"id":7346,"depth":57,"text":7347},{"id":7433,"depth":57,"text":7434},{"id":7477,"depth":57,"text":7478},{"id":3126,"depth":57,"text":3127},"2025-05-11","Navigating Python's ecosystem as a Node.js developer — package management, virtual environments, and the tools that bridge the gap.",{},"\u002Fwritings\u002Fa-node-js-developers-journey-into-python-territory",{"title":7110,"description":7588},"writings\u002Fa-node-js-developers-journey-into-python-territory","h5JdyOqwrdA4zuw3SZWjt1YBZ9J4twlT-vUzIaCDSjc",{"id":7595,"title":7596,"body":7597,"category":3161,"date":10173,"description":10174,"draft":1462,"extension":1463,"meta":10175,"navigation":378,"path":10176,"seo":10177,"sitemap":378,"stem":10178,"__hash__":10179},"writings\u002Fwritings\u002Ftired-of-vue-boilerplate-heres-my-clean-fast-setup.md","Tired of Vue Boilerplate? Here's My Clean, Fast Setup",{"type":7,"value":7598,"toc":10154},[7599,7602,7606,7616,7648,7651,7655,7659,7662,7700,7706,7710,7730,7736,7875,7880,7919,7923,7960,7966,8242,8246,8323,8327,8344,8350,8564,8571,8592,8596,8613,8618,8853,8859,9028,9034,9084,9089,9268,9275,9415,9421,9455,9459,9462,9479,9482,9485,9503,9509,9635,9639,9643,9646,9660,9664,9682,9767,9771,9774,9788,9792,9795,10072,10076,10079,10104,10108,10111,10128,10131,10142,10151],[10,7600,7601],{},"After setting up Vue projects from scratch for clients more times than I can count — installing dependencies, configuring ESLint, setting up TypeScript, adding Prettier — I built a streamlined starter that skips the repetitive work.",[108,7603,7605],{"id":7604},"the-pain-points","The Pain Points",[10,7607,7608,7609,5662,7612,7615],{},"If you've worked with Vue, you know the drill. You run ",[21,7610,7611],{},"create-vue",[21,7613,7614],{},"vite create",", and then spend the next hour customizing the setup:",[6840,7617,7618,7624,7630,7636,7642],{},[1405,7619,7620,7623],{},[265,7621,7622],{},"Adding proper TypeScript configuration"," that actually works with Vue",[1405,7625,7626,7629],{},[265,7627,7628],{},"Setting up linting rules"," that don't drive you crazy",[1405,7631,7632,7635],{},[265,7633,7634],{},"Configuring file-based routing"," because manually defining routes is so 2018",[1405,7637,7638,7641],{},[265,7639,7640],{},"Integrating UI components"," that don't need endless styling from scratch",[1405,7643,7644,7647],{},[265,7645,7646],{},"Tweaking performance settings"," you'll forget about until things slow down",[10,7649,7650],{},"Nuxt works for some projects, but it's often more than what's needed. Sometimes a clean Vue setup without the extra abstraction layer is the right call.",[108,7652,7654],{"id":7653},"the-setup","The Setup",[2795,7656,7658],{"id":7657},"step-1-create-the-base-project","Step 1: Create the Base Project",[10,7660,7661],{},"Start with Vite:",[14,7663,7665],{"className":1330,"code":7664,"language":1332,"meta":19,"style":19},"bun create vite@latest my-project --template vue-ts\ncd my-project\nbun install\n",[21,7666,7667,7687,7693],{"__ignoreMap":19},[24,7668,7669,7672,7675,7678,7681,7684],{"class":26,"line":27},[24,7670,7671],{"class":389},"bun",[24,7673,7674],{"class":350}," create",[24,7676,7677],{"class":350}," vite@latest",[24,7679,7680],{"class":350}," my-project",[24,7682,7683],{"class":3231}," --template",[24,7685,7686],{"class":350}," vue-ts\n",[24,7688,7689,7691],{"class":26,"line":57},[24,7690,3357],{"class":38},[24,7692,3352],{"class":350},[24,7694,7695,7697],{"class":26,"line":78},[24,7696,7671],{"class":389},[24,7698,7699],{"class":350}," install\n",[7701,7702,7703],"blockquote",{},[10,7704,7705],{},"I use Bun for package management — faster and has built-in TypeScript support. npm\u002Fyarn work fine too.",[2795,7707,7709],{"id":7708},"step-2-add-prettier","Step 2: Add Prettier",[14,7711,7713],{"className":1330,"code":7712,"language":1332,"meta":19,"style":19},"bun add -D prettier prettier-plugin-tailwindcss\n",[21,7714,7715],{"__ignoreMap":19},[24,7716,7717,7719,7721,7724,7727],{"class":26,"line":27},[24,7718,7671],{"class":389},[24,7720,1432],{"class":350},[24,7722,7723],{"class":3231}," -D",[24,7725,7726],{"class":350}," prettier",[24,7728,7729],{"class":350}," prettier-plugin-tailwindcss\n",[10,7731,7732,7733,2952],{},"Create a ",[21,7734,7735],{},".prettierrc",[14,7737,7739],{"className":6917,"code":7738,"language":6919,"meta":19,"style":19},"{\n  \"printWidth\": 100,\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"trailingComma\": \"all\",\n  \"endOfLine\": \"lf\",\n  \"plugins\": [\"prettier-plugin-tailwindcss\"]\n}\n",[21,7740,7741,7745,7761,7777,7793,7809,7829,7849,7871],{"__ignoreMap":19},[24,7742,7743],{"class":26,"line":27},[24,7744,6926],{"class":34},[24,7746,7747,7749,7752,7754,7756,7759],{"class":26,"line":57},[24,7748,6932],{"class":6931},[24,7750,7751],{"class":38},"printWidth",[24,7753,4566],{"class":6931},[24,7755,2952],{"class":34},[24,7757,7758],{"class":446}," 100",[24,7760,4637],{"class":34},[24,7762,7763,7765,7768,7770,7772,7775],{"class":26,"line":78},[24,7764,6932],{"class":6931},[24,7766,7767],{"class":38},"tabWidth",[24,7769,4566],{"class":6931},[24,7771,2952],{"class":34},[24,7773,7774],{"class":446}," 2",[24,7776,4637],{"class":34},[24,7778,7779,7781,7784,7786,7788,7791],{"class":26,"line":226},[24,7780,6932],{"class":6931},[24,7782,7783],{"class":38},"semi",[24,7785,4566],{"class":6931},[24,7787,2952],{"class":34},[24,7789,7790],{"class":30}," true",[24,7792,4637],{"class":34},[24,7794,7795,7797,7800,7802,7804,7807],{"class":26,"line":238},[24,7796,6932],{"class":6931},[24,7798,7799],{"class":38},"singleQuote",[24,7801,4566],{"class":6931},[24,7803,2952],{"class":34},[24,7805,7806],{"class":30}," false",[24,7808,4637],{"class":34},[24,7810,7811,7813,7816,7818,7820,7822,7825,7827],{"class":26,"line":244},[24,7812,6932],{"class":6931},[24,7814,7815],{"class":38},"trailingComma",[24,7817,4566],{"class":6931},[24,7819,2952],{"class":34},[24,7821,3287],{"class":346},[24,7823,7824],{"class":350},"all",[24,7826,4566],{"class":346},[24,7828,4637],{"class":34},[24,7830,7831,7833,7836,7838,7840,7842,7845,7847],{"class":26,"line":403},[24,7832,6932],{"class":6931},[24,7834,7835],{"class":38},"endOfLine",[24,7837,4566],{"class":6931},[24,7839,2952],{"class":34},[24,7841,3287],{"class":346},[24,7843,7844],{"class":350},"lf",[24,7846,4566],{"class":346},[24,7848,4637],{"class":34},[24,7850,7851,7853,7856,7858,7860,7862,7864,7867,7869],{"class":26,"line":422},[24,7852,6932],{"class":6931},[24,7854,7855],{"class":38},"plugins",[24,7857,4566],{"class":6931},[24,7859,2952],{"class":34},[24,7861,6724],{"class":34},[24,7863,4566],{"class":346},[24,7865,7866],{"class":350},"prettier-plugin-tailwindcss",[24,7868,4566],{"class":346},[24,7870,5007],{"class":34},[24,7872,7873],{"class":26,"line":453},[24,7874,247],{"class":34},[10,7876,7877,7878,2952],{},"Add to ",[21,7879,5093],{},[14,7881,7883],{"className":6917,"code":7882,"language":6919,"meta":19,"style":19},"\"scripts\": {\n  \"format\": \"prettier --write . --list-different --cache\"\n}\n",[21,7884,7885,7897,7915],{"__ignoreMap":19},[24,7886,7887,7889,7891,7893,7895],{"class":26,"line":27},[24,7888,4566],{"class":346},[24,7890,6935],{"class":350},[24,7892,4566],{"class":346},[24,7894,220],{"class":7248},[24,7896,6926],{"class":34},[24,7898,7899,7901,7904,7906,7908,7910,7913],{"class":26,"line":57},[24,7900,6932],{"class":6931},[24,7902,7903],{"class":38},"format",[24,7905,4566],{"class":6931},[24,7907,2952],{"class":34},[24,7909,3287],{"class":346},[24,7911,7912],{"class":350},"prettier --write . --list-different --cache",[24,7914,3293],{"class":346},[24,7916,7917],{"class":26,"line":78},[24,7918,247],{"class":34},[2795,7920,7922],{"id":7921},"step-3-eslint-configuration","Step 3: ESLint Configuration",[14,7924,7926],{"className":1330,"code":7925,"language":1332,"meta":19,"style":19},"bun add -D eslint @eslint\u002Fjs globals typescript-eslint eslint-plugin-perfectionist eslint-plugin-prettier eslint-config-prettier eslint-plugin-vue\n",[21,7927,7928],{"__ignoreMap":19},[24,7929,7930,7932,7934,7936,7939,7942,7945,7948,7951,7954,7957],{"class":26,"line":27},[24,7931,7671],{"class":389},[24,7933,1432],{"class":350},[24,7935,7723],{"class":3231},[24,7937,7938],{"class":350}," eslint",[24,7940,7941],{"class":350}," @eslint\u002Fjs",[24,7943,7944],{"class":350}," globals",[24,7946,7947],{"class":350}," typescript-eslint",[24,7949,7950],{"class":350}," eslint-plugin-perfectionist",[24,7952,7953],{"class":350}," eslint-plugin-prettier",[24,7955,7956],{"class":350}," eslint-config-prettier",[24,7958,7959],{"class":350}," eslint-plugin-vue\n",[10,7961,7962,7963,2952],{},"Create ",[21,7964,7965],{},"eslint.config.mjs",[14,7967,7971],{"className":7968,"code":7969,"language":7970,"meta":19,"style":19},"language-javascript shiki shiki-themes vitesse-light vitesse-dark","import eslint from \"@eslint\u002Fjs\";\nimport perfectionist from \"eslint-plugin-perfectionist\";\nimport prettier from \"eslint-plugin-prettier\u002Frecommended\";\nimport vue from \"eslint-plugin-vue\";\nimport globals from \"globals\";\nimport tseslint from \"typescript-eslint\";\n\nconst eslintConfig = tseslint.config(\n  eslint.configs.recommended,\n  perfectionist.configs[\"recommended-natural\"],\n  tseslint.configs.recommended,\n  {\n    extends: [...vue.configs[\"flat\u002Frecommended\"]],\n    files: [\"**\u002F*.{ts,vue}\"],\n    languageOptions: {\n      ecmaVersion: \"latest\",\n      globals: {\n        ...globals.browser,\n      },\n      parserOptions: {\n        parser: tseslint.parser,\n      },\n      sourceType: \"module\",\n    },\n    rules: {\n      \"no-console\": [\"warn\", { allow: [\"warn\", \"error\"] }],\n      \"vue\u002Fmulti-word-component-names\": \"off\",\n    },\n  },\n  {\n    rules: {\n      \"@typescript-eslint\u002Fconsistent-type-definitions\": [\"error\", \"type\"],\n      \"@typescript-eslint\u002Fconsistent-type-imports\": [\n        \"error\",\n        {\n          fixStyle: \"separate-type-imports\",\n          prefer: \"type-imports\",\n        },\n      ],\n      \"@typescript-eslint\u002Fno-unused-vars\": [\n        \"warn\",\n        {\n          argsIgnorePattern: \"^_\",\n          ignoreRestSiblings: true,\n        },\n      ],\n    },\n  },\n  prettier,\n);\n\nexport default eslintConfig;\n","javascript",[21,7972,7973,7978,7983,7988,7993,7998,8003,8007,8012,8017,8022,8027,8032,8037,8042,8047,8052,8057,8062,8067,8072,8077,8081,8086,8091,8096,8101,8106,8110,8114,8118,8123,8129,8135,8141,8147,8153,8159,8165,8171,8177,8183,8188,8194,8200,8205,8210,8215,8220,8226,8231,8236],{"__ignoreMap":19},[24,7974,7975],{"class":26,"line":27},[24,7976,7977],{},"import eslint from \"@eslint\u002Fjs\";\n",[24,7979,7980],{"class":26,"line":57},[24,7981,7982],{},"import perfectionist from \"eslint-plugin-perfectionist\";\n",[24,7984,7985],{"class":26,"line":78},[24,7986,7987],{},"import prettier from \"eslint-plugin-prettier\u002Frecommended\";\n",[24,7989,7990],{"class":26,"line":226},[24,7991,7992],{},"import vue from \"eslint-plugin-vue\";\n",[24,7994,7995],{"class":26,"line":238},[24,7996,7997],{},"import globals from \"globals\";\n",[24,7999,8000],{"class":26,"line":244},[24,8001,8002],{},"import tseslint from \"typescript-eslint\";\n",[24,8004,8005],{"class":26,"line":403},[24,8006,379],{"emptyLinePlaceholder":378},[24,8008,8009],{"class":26,"line":422},[24,8010,8011],{},"const eslintConfig = tseslint.config(\n",[24,8013,8014],{"class":26,"line":453},[24,8015,8016],{},"  eslint.configs.recommended,\n",[24,8018,8019],{"class":26,"line":490},[24,8020,8021],{},"  perfectionist.configs[\"recommended-natural\"],\n",[24,8023,8024],{"class":26,"line":496},[24,8025,8026],{},"  tseslint.configs.recommended,\n",[24,8028,8029],{"class":26,"line":504},[24,8030,8031],{},"  {\n",[24,8033,8034],{"class":26,"line":520},[24,8035,8036],{},"    extends: [...vue.configs[\"flat\u002Frecommended\"]],\n",[24,8038,8039],{"class":26,"line":545},[24,8040,8041],{},"    files: [\"**\u002F*.{ts,vue}\"],\n",[24,8043,8044],{"class":26,"line":571},[24,8045,8046],{},"    languageOptions: {\n",[24,8048,8049],{"class":26,"line":640},[24,8050,8051],{},"      ecmaVersion: \"latest\",\n",[24,8053,8054],{"class":26,"line":645},[24,8055,8056],{},"      globals: {\n",[24,8058,8059],{"class":26,"line":5039},[24,8060,8061],{},"        ...globals.browser,\n",[24,8063,8064],{"class":26,"line":5045},[24,8065,8066],{},"      },\n",[24,8068,8069],{"class":26,"line":5051},[24,8070,8071],{},"      parserOptions: {\n",[24,8073,8074],{"class":26,"line":5056},[24,8075,8076],{},"        parser: tseslint.parser,\n",[24,8078,8079],{"class":26,"line":5061},[24,8080,8066],{},[24,8082,8083],{"class":26,"line":5067},[24,8084,8085],{},"      sourceType: \"module\",\n",[24,8087,8088],{"class":26,"line":5073},[24,8089,8090],{},"    },\n",[24,8092,8093],{"class":26,"line":5236},[24,8094,8095],{},"    rules: {\n",[24,8097,8098],{"class":26,"line":5525},[24,8099,8100],{},"      \"no-console\": [\"warn\", { allow: [\"warn\", \"error\"] }],\n",[24,8102,8103],{"class":26,"line":5531},[24,8104,8105],{},"      \"vue\u002Fmulti-word-component-names\": \"off\",\n",[24,8107,8108],{"class":26,"line":5537},[24,8109,8090],{},[24,8111,8112],{"class":26,"line":5543},[24,8113,493],{},[24,8115,8116],{"class":26,"line":5549},[24,8117,8031],{},[24,8119,8121],{"class":26,"line":8120},31,[24,8122,8095],{},[24,8124,8126],{"class":26,"line":8125},32,[24,8127,8128],{},"      \"@typescript-eslint\u002Fconsistent-type-definitions\": [\"error\", \"type\"],\n",[24,8130,8132],{"class":26,"line":8131},33,[24,8133,8134],{},"      \"@typescript-eslint\u002Fconsistent-type-imports\": [\n",[24,8136,8138],{"class":26,"line":8137},34,[24,8139,8140],{},"        \"error\",\n",[24,8142,8144],{"class":26,"line":8143},35,[24,8145,8146],{},"        {\n",[24,8148,8150],{"class":26,"line":8149},36,[24,8151,8152],{},"          fixStyle: \"separate-type-imports\",\n",[24,8154,8156],{"class":26,"line":8155},37,[24,8157,8158],{},"          prefer: \"type-imports\",\n",[24,8160,8162],{"class":26,"line":8161},38,[24,8163,8164],{},"        },\n",[24,8166,8168],{"class":26,"line":8167},39,[24,8169,8170],{},"      ],\n",[24,8172,8174],{"class":26,"line":8173},40,[24,8175,8176],{},"      \"@typescript-eslint\u002Fno-unused-vars\": [\n",[24,8178,8180],{"class":26,"line":8179},41,[24,8181,8182],{},"        \"warn\",\n",[24,8184,8186],{"class":26,"line":8185},42,[24,8187,8146],{},[24,8189,8191],{"class":26,"line":8190},43,[24,8192,8193],{},"          argsIgnorePattern: \"^_\",\n",[24,8195,8197],{"class":26,"line":8196},44,[24,8198,8199],{},"          ignoreRestSiblings: true,\n",[24,8201,8203],{"class":26,"line":8202},45,[24,8204,8164],{},[24,8206,8208],{"class":26,"line":8207},46,[24,8209,8170],{},[24,8211,8213],{"class":26,"line":8212},47,[24,8214,8090],{},[24,8216,8218],{"class":26,"line":8217},48,[24,8219,493],{},[24,8221,8223],{"class":26,"line":8222},49,[24,8224,8225],{},"  prettier,\n",[24,8227,8229],{"class":26,"line":8228},50,[24,8230,6717],{},[24,8232,8234],{"class":26,"line":8233},51,[24,8235,379],{"emptyLinePlaceholder":378},[24,8237,8239],{"class":26,"line":8238},52,[24,8240,8241],{},"export default eslintConfig;\n",[10,8243,7877,8244,2952],{},[21,8245,5093],{},[14,8247,8249],{"className":6917,"code":8248,"language":6919,"meta":19,"style":19},"\"scripts\": {\n  \"lint\": \"eslint src\",\n  \"lint:fix\": \"eslint src --fix\",\n  \"typecheck\": \"vue-tsc --noEmit\"\n}\n",[21,8250,8251,8263,8282,8302,8319],{"__ignoreMap":19},[24,8252,8253,8255,8257,8259,8261],{"class":26,"line":27},[24,8254,4566],{"class":346},[24,8256,6935],{"class":350},[24,8258,4566],{"class":346},[24,8260,220],{"class":7248},[24,8262,6926],{"class":34},[24,8264,8265,8267,8269,8271,8273,8275,8278,8280],{"class":26,"line":57},[24,8266,6932],{"class":6931},[24,8268,6989],{"class":38},[24,8270,4566],{"class":6931},[24,8272,2952],{"class":34},[24,8274,3287],{"class":346},[24,8276,8277],{"class":350},"eslint src",[24,8279,4566],{"class":346},[24,8281,4637],{"class":34},[24,8283,8284,8286,8289,8291,8293,8295,8298,8300],{"class":26,"line":78},[24,8285,6932],{"class":6931},[24,8287,8288],{"class":38},"lint:fix",[24,8290,4566],{"class":6931},[24,8292,2952],{"class":34},[24,8294,3287],{"class":346},[24,8296,8297],{"class":350},"eslint src --fix",[24,8299,4566],{"class":346},[24,8301,4637],{"class":34},[24,8303,8304,8306,8308,8310,8312,8314,8317],{"class":26,"line":226},[24,8305,6932],{"class":6931},[24,8307,7009],{"class":38},[24,8309,4566],{"class":6931},[24,8311,2952],{"class":34},[24,8313,3287],{"class":346},[24,8315,8316],{"class":350},"vue-tsc --noEmit",[24,8318,3293],{"class":346},[24,8320,8321],{"class":26,"line":238},[24,8322,247],{"class":34},[2795,8324,8326],{"id":8325},"step-4-tailwindcss","Step 4: TailwindCSS",[14,8328,8330],{"className":1330,"code":8329,"language":1332,"meta":19,"style":19},"bun add tailwindcss @tailwindcss\u002Fvite\n",[21,8331,8332],{"__ignoreMap":19},[24,8333,8334,8336,8338,8341],{"class":26,"line":27},[24,8335,7671],{"class":389},[24,8337,1432],{"class":350},[24,8339,8340],{"class":350}," tailwindcss",[24,8342,8343],{"class":350}," @tailwindcss\u002Fvite\n",[10,8345,8346,8347,2952],{},"Update your ",[21,8348,8349],{},"vite.config.ts",[14,8351,8353],{"className":4543,"code":8352,"language":4545,"meta":19,"style":19},"import tailwindcss from \"@tailwindcss\u002Fvite\";\nimport vue from \"@vitejs\u002Fplugin-vue\";\nimport path from \"node:path\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [vue(), tailwindcss()],\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \".\u002Fsrc\"),\n    },\n  },\n  server: {\n    port: 3000,\n    hmr: {\n      overlay: true,\n    },\n  },\n});\n",[21,8354,8355,8372,8390,8408,8428,8432,8442,8458,8465,8472,8508,8512,8516,8522,8534,8541,8552,8556,8560],{"__ignoreMap":19},[24,8356,8357,8359,8361,8363,8365,8368,8370],{"class":26,"line":27},[24,8358,31],{"class":30},[24,8360,8340],{"class":44},[24,8362,343],{"class":30},[24,8364,3287],{"class":346},[24,8366,8367],{"class":350},"@tailwindcss\u002Fvite",[24,8369,4566],{"class":346},[24,8371,4569],{"class":34},[24,8373,8374,8376,8379,8381,8383,8386,8388],{"class":26,"line":57},[24,8375,31],{"class":30},[24,8377,8378],{"class":44}," vue",[24,8380,343],{"class":30},[24,8382,3287],{"class":346},[24,8384,8385],{"class":350},"@vitejs\u002Fplugin-vue",[24,8387,4566],{"class":346},[24,8389,4569],{"class":34},[24,8391,8392,8394,8397,8399,8401,8404,8406],{"class":26,"line":78},[24,8393,31],{"class":30},[24,8395,8396],{"class":44}," path",[24,8398,343],{"class":30},[24,8400,3287],{"class":346},[24,8402,8403],{"class":350},"node:path",[24,8405,4566],{"class":346},[24,8407,4569],{"class":34},[24,8409,8410,8412,8414,8416,8418,8420,8422,8424,8426],{"class":26,"line":226},[24,8411,31],{"class":30},[24,8413,334],{"class":34},[24,8415,683],{"class":44},[24,8417,340],{"class":34},[24,8419,343],{"class":30},[24,8421,3287],{"class":346},[24,8423,692],{"class":350},[24,8425,4566],{"class":346},[24,8427,4569],{"class":34},[24,8429,8430],{"class":26,"line":238},[24,8431,379],{"emptyLinePlaceholder":378},[24,8433,8434,8436,8438,8440],{"class":26,"line":244},[24,8435,148],{"class":30},[24,8437,386],{"class":30},[24,8439,683],{"class":389},[24,8441,392],{"class":34},[24,8443,8444,8446,8448,8451,8453,8456],{"class":26,"line":403},[24,8445,713],{"class":38},[24,8447,716],{"class":34},[24,8449,8450],{"class":389},"vue",[24,8452,1016],{"class":34},[24,8454,8455],{"class":389},"tailwindcss",[24,8457,722],{"class":34},[24,8459,8460,8463],{"class":26,"line":422},[24,8461,8462],{"class":38},"  resolve",[24,8464,400],{"class":34},[24,8466,8467,8470],{"class":26,"line":453},[24,8468,8469],{"class":38},"    alias",[24,8471,400],{"class":34},[24,8473,8474,8477,8480,8482,8484,8487,8489,8492,8494,8497,8499,8501,8504,8506],{"class":26,"line":490},[24,8475,8476],{"class":346},"      \"",[24,8478,8479],{"class":350},"@",[24,8481,4566],{"class":346},[24,8483,220],{"class":34},[24,8485,8486],{"class":44},"path",[24,8488,35],{"class":34},[24,8490,8491],{"class":389},"resolve",[24,8493,443],{"class":34},[24,8495,8496],{"class":44},"__dirname",[24,8498,597],{"class":34},[24,8500,4566],{"class":346},[24,8502,8503],{"class":350},".\u002Fsrc",[24,8505,4566],{"class":346},[24,8507,450],{"class":34},[24,8509,8510],{"class":26,"line":496},[24,8511,8090],{"class":34},[24,8513,8514],{"class":26,"line":504},[24,8515,493],{"class":34},[24,8517,8518,8520],{"class":26,"line":520},[24,8519,397],{"class":38},[24,8521,400],{"class":34},[24,8523,8524,8527,8529,8532],{"class":26,"line":545},[24,8525,8526],{"class":38},"    port",[24,8528,220],{"class":34},[24,8530,8531],{"class":446},"3000",[24,8533,4637],{"class":34},[24,8535,8536,8539],{"class":26,"line":571},[24,8537,8538],{"class":38},"    hmr",[24,8540,400],{"class":34},[24,8542,8543,8546,8548,8550],{"class":26,"line":640},[24,8544,8545],{"class":38},"      overlay",[24,8547,220],{"class":34},[24,8549,4634],{"class":30},[24,8551,4637],{"class":34},[24,8553,8554],{"class":26,"line":645},[24,8555,8090],{"class":34},[24,8557,8558],{"class":26,"line":5039},[24,8559,493],{"class":34},[24,8561,8562],{"class":26,"line":5045},[24,8563,4653],{"class":34},[10,8565,8566,8567,8570],{},"Replace your ",[21,8568,8569],{},"src\u002Fstyle.css"," with:",[14,8572,8576],{"className":8573,"code":8574,"language":8575,"meta":19,"style":19},"language-css shiki shiki-themes vitesse-light vitesse-dark","@import \"tailwindcss\";\n","css",[21,8577,8578],{"__ignoreMap":19},[24,8579,8580,8582,8584,8586,8588,8590],{"class":26,"line":27},[24,8581,8479],{"class":34},[24,8583,31],{"class":30},[24,8585,3287],{"class":346},[24,8587,8455],{"class":350},[24,8589,4566],{"class":346},[24,8591,4569],{"class":34},[2795,8593,8595],{"id":8594},"step-5-file-based-routing","Step 5: File-Based Routing",[14,8597,8599],{"className":1330,"code":8598,"language":1332,"meta":19,"style":19},"bun add vue-router unplugin-vue-router\n",[21,8600,8601],{"__ignoreMap":19},[24,8602,8603,8605,8607,8610],{"class":26,"line":27},[24,8604,7671],{"class":389},[24,8606,1432],{"class":350},[24,8608,8609],{"class":350}," vue-router",[24,8611,8612],{"class":350}," unplugin-vue-router\n",[10,8614,8346,8615,8617],{},[21,8616,8349],{}," again:",[14,8619,8621],{"className":4543,"code":8620,"language":4545,"meta":19,"style":19},"import tailwindcss from \"@tailwindcss\u002Fvite\";\nimport vue from \"@vitejs\u002Fplugin-vue\";\nimport path from \"node:path\";\nimport router from \"unplugin-vue-router\u002Fvite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [\n    router(), \u002F\u002F Must come before vue()\n    vue(),\n    tailwindcss(),\n  ],\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \".\u002Fsrc\"),\n    },\n  },\n  server: {\n    port: 3000,\n    hmr: {\n      overlay: true,\n    },\n  },\n});\n",[21,8622,8623,8639,8655,8671,8689,8709,8713,8723,8730,8740,8747,8754,8759,8765,8771,8801,8805,8809,8815,8825,8831,8841,8845,8849],{"__ignoreMap":19},[24,8624,8625,8627,8629,8631,8633,8635,8637],{"class":26,"line":27},[24,8626,31],{"class":30},[24,8628,8340],{"class":44},[24,8630,343],{"class":30},[24,8632,3287],{"class":346},[24,8634,8367],{"class":350},[24,8636,4566],{"class":346},[24,8638,4569],{"class":34},[24,8640,8641,8643,8645,8647,8649,8651,8653],{"class":26,"line":57},[24,8642,31],{"class":30},[24,8644,8378],{"class":44},[24,8646,343],{"class":30},[24,8648,3287],{"class":346},[24,8650,8385],{"class":350},[24,8652,4566],{"class":346},[24,8654,4569],{"class":34},[24,8656,8657,8659,8661,8663,8665,8667,8669],{"class":26,"line":78},[24,8658,31],{"class":30},[24,8660,8396],{"class":44},[24,8662,343],{"class":30},[24,8664,3287],{"class":346},[24,8666,8403],{"class":350},[24,8668,4566],{"class":346},[24,8670,4569],{"class":34},[24,8672,8673,8675,8678,8680,8682,8685,8687],{"class":26,"line":226},[24,8674,31],{"class":30},[24,8676,8677],{"class":44}," router",[24,8679,343],{"class":30},[24,8681,3287],{"class":346},[24,8683,8684],{"class":350},"unplugin-vue-router\u002Fvite",[24,8686,4566],{"class":346},[24,8688,4569],{"class":34},[24,8690,8691,8693,8695,8697,8699,8701,8703,8705,8707],{"class":26,"line":238},[24,8692,31],{"class":30},[24,8694,334],{"class":34},[24,8696,683],{"class":44},[24,8698,340],{"class":34},[24,8700,343],{"class":30},[24,8702,3287],{"class":346},[24,8704,692],{"class":350},[24,8706,4566],{"class":346},[24,8708,4569],{"class":34},[24,8710,8711],{"class":26,"line":244},[24,8712,379],{"emptyLinePlaceholder":378},[24,8714,8715,8717,8719,8721],{"class":26,"line":403},[24,8716,148],{"class":30},[24,8718,386],{"class":30},[24,8720,683],{"class":389},[24,8722,392],{"class":34},[24,8724,8725,8727],{"class":26,"line":422},[24,8726,713],{"class":38},[24,8728,8729],{"class":34},": [\n",[24,8731,8732,8735,8737],{"class":26,"line":453},[24,8733,8734],{"class":389},"    router",[24,8736,1016],{"class":34},[24,8738,8739],{"class":53},"\u002F\u002F Must come before vue()\n",[24,8741,8742,8745],{"class":26,"line":490},[24,8743,8744],{"class":389},"    vue",[24,8746,419],{"class":34},[24,8748,8749,8752],{"class":26,"line":496},[24,8750,8751],{"class":389},"    tailwindcss",[24,8753,419],{"class":34},[24,8755,8756],{"class":26,"line":504},[24,8757,8758],{"class":34},"  ],\n",[24,8760,8761,8763],{"class":26,"line":520},[24,8762,8462],{"class":38},[24,8764,400],{"class":34},[24,8766,8767,8769],{"class":26,"line":545},[24,8768,8469],{"class":38},[24,8770,400],{"class":34},[24,8772,8773,8775,8777,8779,8781,8783,8785,8787,8789,8791,8793,8795,8797,8799],{"class":26,"line":571},[24,8774,8476],{"class":346},[24,8776,8479],{"class":350},[24,8778,4566],{"class":346},[24,8780,220],{"class":34},[24,8782,8486],{"class":44},[24,8784,35],{"class":34},[24,8786,8491],{"class":389},[24,8788,443],{"class":34},[24,8790,8496],{"class":44},[24,8792,597],{"class":34},[24,8794,4566],{"class":346},[24,8796,8503],{"class":350},[24,8798,4566],{"class":346},[24,8800,450],{"class":34},[24,8802,8803],{"class":26,"line":640},[24,8804,8090],{"class":34},[24,8806,8807],{"class":26,"line":645},[24,8808,493],{"class":34},[24,8810,8811,8813],{"class":26,"line":5039},[24,8812,397],{"class":38},[24,8814,400],{"class":34},[24,8816,8817,8819,8821,8823],{"class":26,"line":5045},[24,8818,8526],{"class":38},[24,8820,220],{"class":34},[24,8822,8531],{"class":446},[24,8824,4637],{"class":34},[24,8826,8827,8829],{"class":26,"line":5051},[24,8828,8538],{"class":38},[24,8830,400],{"class":34},[24,8832,8833,8835,8837,8839],{"class":26,"line":5056},[24,8834,8545],{"class":38},[24,8836,220],{"class":34},[24,8838,4634],{"class":30},[24,8840,4637],{"class":34},[24,8842,8843],{"class":26,"line":5061},[24,8844,8090],{"class":34},[24,8846,8847],{"class":26,"line":5067},[24,8848,493],{"class":34},[24,8850,8851],{"class":26,"line":5073},[24,8852,4653],{"class":34},[10,8854,8855,8856,2952],{},"Update your TypeScript configuration in ",[21,8857,8858],{},"tsconfig.app.json",[14,8860,8862],{"className":6917,"code":8861,"language":6919,"meta":19,"style":19},"{\n  \"extends\": \"@vue\u002Ftsconfig\u002Ftsconfig.dom.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@\u002F*\": [\".\u002Fsrc\u002F*\"]\n    },\n    \"types\": [\"unplugin-vue-router\u002Fclient\"]\n  },\n  \"include\": [\"src\u002F**\u002F*.ts\", \"src\u002F**\u002F*.vue\", \".\u002Ftyped-router.d.ts\"]\n}\n",[21,8863,8864,8868,8887,8900,8919,8932,8954,8958,8980,8984,9024],{"__ignoreMap":19},[24,8865,8866],{"class":26,"line":27},[24,8867,6926],{"class":34},[24,8869,8870,8872,8874,8876,8878,8880,8883,8885],{"class":26,"line":57},[24,8871,6932],{"class":6931},[24,8873,291],{"class":38},[24,8875,4566],{"class":6931},[24,8877,2952],{"class":34},[24,8879,3287],{"class":346},[24,8881,8882],{"class":350},"@vue\u002Ftsconfig\u002Ftsconfig.dom.json",[24,8884,4566],{"class":346},[24,8886,4637],{"class":34},[24,8888,8889,8891,8894,8896,8898],{"class":26,"line":78},[24,8890,6932],{"class":6931},[24,8892,8893],{"class":38},"compilerOptions",[24,8895,4566],{"class":6931},[24,8897,2952],{"class":34},[24,8899,209],{"class":34},[24,8901,8902,8904,8907,8909,8911,8913,8915,8917],{"class":26,"line":226},[24,8903,6946],{"class":6931},[24,8905,8906],{"class":38},"baseUrl",[24,8908,4566],{"class":6931},[24,8910,2952],{"class":34},[24,8912,3287],{"class":346},[24,8914,35],{"class":350},[24,8916,4566],{"class":346},[24,8918,4637],{"class":34},[24,8920,8921,8923,8926,8928,8930],{"class":26,"line":238},[24,8922,6946],{"class":6931},[24,8924,8925],{"class":38},"paths",[24,8927,4566],{"class":6931},[24,8929,2952],{"class":34},[24,8931,209],{"class":34},[24,8933,8934,8936,8939,8941,8943,8945,8947,8950,8952],{"class":26,"line":244},[24,8935,8476],{"class":6931},[24,8937,8938],{"class":38},"@\u002F*",[24,8940,4566],{"class":6931},[24,8942,2952],{"class":34},[24,8944,6724],{"class":34},[24,8946,4566],{"class":346},[24,8948,8949],{"class":350},".\u002Fsrc\u002F*",[24,8951,4566],{"class":346},[24,8953,5007],{"class":34},[24,8955,8956],{"class":26,"line":403},[24,8957,8090],{"class":34},[24,8959,8960,8962,8965,8967,8969,8971,8973,8976,8978],{"class":26,"line":422},[24,8961,6946],{"class":6931},[24,8963,8964],{"class":38},"types",[24,8966,4566],{"class":6931},[24,8968,2952],{"class":34},[24,8970,6724],{"class":34},[24,8972,4566],{"class":346},[24,8974,8975],{"class":350},"unplugin-vue-router\u002Fclient",[24,8977,4566],{"class":346},[24,8979,5007],{"class":34},[24,8981,8982],{"class":26,"line":453},[24,8983,493],{"class":34},[24,8985,8986,8988,8991,8993,8995,8997,8999,9002,9004,9006,9008,9011,9013,9015,9017,9020,9022],{"class":26,"line":490},[24,8987,6932],{"class":6931},[24,8989,8990],{"class":38},"include",[24,8992,4566],{"class":6931},[24,8994,2952],{"class":34},[24,8996,6724],{"class":34},[24,8998,4566],{"class":346},[24,9000,9001],{"class":350},"src\u002F**\u002F*.ts",[24,9003,4566],{"class":346},[24,9005,6730],{"class":34},[24,9007,3287],{"class":346},[24,9009,9010],{"class":350},"src\u002F**\u002F*.vue",[24,9012,4566],{"class":346},[24,9014,6730],{"class":34},[24,9016,3287],{"class":346},[24,9018,9019],{"class":350},".\u002Ftyped-router.d.ts",[24,9021,4566],{"class":346},[24,9023,5007],{"class":34},[24,9025,9026],{"class":26,"line":496},[24,9027,247],{"class":34},[10,9029,9030,9031,2952],{},"Add type references to ",[21,9032,9033],{},"src\u002Fvite-env.d.ts",[14,9035,9037],{"className":4543,"code":9036,"language":4545,"meta":19,"style":19},"\u002F\u002F\u002F \u003Creference types=\"vite\u002Fclient\" \u002F>\n\u002F\u002F\u002F \u003Creference types=\"unplugin-vue-router\u002Fclient\" \u002F>\n",[21,9038,9039,9064],{"__ignoreMap":19},[24,9040,9041,9044,9046,9049,9052,9054,9056,9059,9061],{"class":26,"line":27},[24,9042,9043],{"class":53},"\u002F\u002F\u002F ",[24,9045,6208],{"class":34},[24,9047,9048],{"class":30},"reference",[24,9050,9051],{"class":44}," types",[24,9053,6407],{"class":34},[24,9055,4566],{"class":346},[24,9057,9058],{"class":350},"vite\u002Fclient",[24,9060,4566],{"class":346},[24,9062,9063],{"class":34}," \u002F>\n",[24,9065,9066,9068,9070,9072,9074,9076,9078,9080,9082],{"class":26,"line":57},[24,9067,9043],{"class":53},[24,9069,6208],{"class":34},[24,9071,9048],{"class":30},[24,9073,9051],{"class":44},[24,9075,6407],{"class":34},[24,9077,4566],{"class":346},[24,9079,8975],{"class":350},[24,9081,4566],{"class":346},[24,9083,9063],{"class":34},[10,9085,7962,9086,2952],{},[21,9087,9088],{},"src\u002Frouter.ts",[14,9090,9092],{"className":4543,"code":9091,"language":4545,"meta":19,"style":19},"import { createRouter, createWebHistory } from \"vue-router\";\nimport { routes } from \"vue-router\u002Fauto-routes\";\n\nexport const router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes,\n  scrollBehavior(_to, _from, savedPosition) {\n    return savedPosition || { top: 0 };\n  },\n});\n\nexport default router;\n",[21,9093,9094,9121,9143,9147,9162,9191,9198,9221,9246,9250,9254,9258],{"__ignoreMap":19},[24,9095,9096,9098,9100,9103,9105,9108,9110,9112,9114,9117,9119],{"class":26,"line":27},[24,9097,31],{"class":30},[24,9099,334],{"class":34},[24,9101,9102],{"class":44}," createRouter",[24,9104,6730],{"class":34},[24,9106,9107],{"class":44}," createWebHistory",[24,9109,340],{"class":34},[24,9111,343],{"class":30},[24,9113,3287],{"class":346},[24,9115,9116],{"class":350},"vue-router",[24,9118,4566],{"class":346},[24,9120,4569],{"class":34},[24,9122,9123,9125,9127,9130,9132,9134,9136,9139,9141],{"class":26,"line":57},[24,9124,31],{"class":30},[24,9126,334],{"class":34},[24,9128,9129],{"class":44}," routes",[24,9131,340],{"class":34},[24,9133,343],{"class":30},[24,9135,3287],{"class":346},[24,9137,9138],{"class":350},"vue-router\u002Fauto-routes",[24,9140,4566],{"class":346},[24,9142,4569],{"class":34},[24,9144,9145],{"class":26,"line":78},[24,9146,379],{"emptyLinePlaceholder":378},[24,9148,9149,9151,9153,9156,9158,9160],{"class":26,"line":226},[24,9150,148],{"class":30},[24,9152,152],{"class":151},[24,9154,9155],{"class":44},"router",[24,9157,158],{"class":34},[24,9159,9102],{"class":389},[24,9161,392],{"class":34},[24,9163,9164,9167,9169,9172,9174,9176,9178,9180,9182,9184,9186,9189],{"class":26,"line":238},[24,9165,9166],{"class":38},"  history",[24,9168,220],{"class":34},[24,9170,9171],{"class":389},"createWebHistory",[24,9173,443],{"class":34},[24,9175,31],{"class":30},[24,9177,35],{"class":34},[24,9179,39],{"class":38},[24,9181,35],{"class":34},[24,9183,45],{"class":44},[24,9185,35],{"class":34},[24,9187,9188],{"class":44},"BASE_URL",[24,9190,450],{"class":34},[24,9192,9193,9196],{"class":26,"line":244},[24,9194,9195],{"class":44},"  routes",[24,9197,4637],{"class":34},[24,9199,9200,9203,9205,9208,9210,9213,9215,9218],{"class":26,"line":403},[24,9201,9202],{"class":389},"  scrollBehavior",[24,9204,443],{"class":34},[24,9206,9207],{"class":44},"_to",[24,9209,597],{"class":34},[24,9211,9212],{"class":44},"_from",[24,9214,597],{"class":34},[24,9216,9217],{"class":44},"savedPosition",[24,9219,9220],{"class":34},") {\n",[24,9222,9223,9226,9229,9232,9235,9238,9240,9243],{"class":26,"line":422},[24,9224,9225],{"class":30},"    return",[24,9227,9228],{"class":44}," savedPosition",[24,9230,9231],{"class":151}," ||",[24,9233,9234],{"class":34}," { ",[24,9236,9237],{"class":38},"top",[24,9239,220],{"class":34},[24,9241,9242],{"class":446},"0",[24,9244,9245],{"class":34}," };\n",[24,9247,9248],{"class":26,"line":453},[24,9249,493],{"class":34},[24,9251,9252],{"class":26,"line":490},[24,9253,4653],{"class":34},[24,9255,9256],{"class":26,"line":496},[24,9257,379],{"emptyLinePlaceholder":378},[24,9259,9260,9262,9264,9266],{"class":26,"line":504},[24,9261,148],{"class":30},[24,9263,386],{"class":30},[24,9265,8677],{"class":44},[24,9267,4569],{"class":34},[10,9269,9270,9271,9274],{},"Update ",[21,9272,9273],{},"src\u002Fmain.ts"," to use the router:",[14,9276,9278],{"className":4543,"code":9277,"language":4545,"meta":19,"style":19},"import { createApp } from \"vue\";\n\nimport App from \"@\u002FApp.vue\";\nimport router from \"@\u002Frouter\";\nimport \"@\u002Fstyle.css\";\n\nconst app = createApp(App);\n\napp.use(router);\napp.mount(\"#app\");\n",[21,9279,9280,9301,9305,9323,9340,9353,9357,9376,9380,9395],{"__ignoreMap":19},[24,9281,9282,9284,9286,9289,9291,9293,9295,9297,9299],{"class":26,"line":27},[24,9283,31],{"class":30},[24,9285,334],{"class":34},[24,9287,9288],{"class":44}," createApp",[24,9290,340],{"class":34},[24,9292,343],{"class":30},[24,9294,3287],{"class":346},[24,9296,8450],{"class":350},[24,9298,4566],{"class":346},[24,9300,4569],{"class":34},[24,9302,9303],{"class":26,"line":57},[24,9304,379],{"emptyLinePlaceholder":378},[24,9306,9307,9309,9312,9314,9316,9319,9321],{"class":26,"line":78},[24,9308,31],{"class":30},[24,9310,9311],{"class":44}," App",[24,9313,343],{"class":30},[24,9315,3287],{"class":346},[24,9317,9318],{"class":350},"@\u002FApp.vue",[24,9320,4566],{"class":346},[24,9322,4569],{"class":34},[24,9324,9325,9327,9329,9331,9333,9336,9338],{"class":26,"line":226},[24,9326,31],{"class":30},[24,9328,8677],{"class":44},[24,9330,343],{"class":30},[24,9332,3287],{"class":346},[24,9334,9335],{"class":350},"@\u002Frouter",[24,9337,4566],{"class":346},[24,9339,4569],{"class":34},[24,9341,9342,9344,9346,9349,9351],{"class":26,"line":238},[24,9343,31],{"class":30},[24,9345,3287],{"class":346},[24,9347,9348],{"class":350},"@\u002Fstyle.css",[24,9350,4566],{"class":346},[24,9352,4569],{"class":34},[24,9354,9355],{"class":26,"line":244},[24,9356,379],{"emptyLinePlaceholder":378},[24,9358,9359,9362,9365,9367,9369,9371,9374],{"class":26,"line":403},[24,9360,9361],{"class":151},"const ",[24,9363,9364],{"class":44},"app",[24,9366,158],{"class":34},[24,9368,9288],{"class":389},[24,9370,443],{"class":34},[24,9372,9373],{"class":44},"App",[24,9375,6717],{"class":34},[24,9377,9378],{"class":26,"line":422},[24,9379,379],{"emptyLinePlaceholder":378},[24,9381,9382,9384,9386,9389,9391,9393],{"class":26,"line":453},[24,9383,9364],{"class":44},[24,9385,35],{"class":34},[24,9387,9388],{"class":389},"use",[24,9390,443],{"class":34},[24,9392,9155],{"class":44},[24,9394,6717],{"class":34},[24,9396,9397,9399,9401,9404,9406,9408,9411,9413],{"class":26,"line":490},[24,9398,9364],{"class":44},[24,9400,35],{"class":34},[24,9402,9403],{"class":389},"mount",[24,9405,443],{"class":34},[24,9407,4566],{"class":346},[24,9409,9410],{"class":350},"#app",[24,9412,4566],{"class":346},[24,9414,6717],{"class":34},[10,9416,9270,9417,9420],{},[21,9418,9419],{},"src\u002FApp.vue"," to include the router view:",[14,9422,9425],{"className":9423,"code":9424,"language":8450,"meta":19,"style":19},"language-vue shiki shiki-themes vitesse-light vitesse-dark","\u003Ctemplate>\n  \u003CRouterView \u002F>\n\u003C\u002Ftemplate>\n",[21,9426,9427,9436,9446],{"__ignoreMap":19},[24,9428,9429,9431,9434],{"class":26,"line":27},[24,9430,6208],{"class":34},[24,9432,9433],{"class":30},"template",[24,9435,6417],{"class":34},[24,9437,9438,9441,9444],{"class":26,"line":57},[24,9439,9440],{"class":34},"  \u003C",[24,9442,9443],{"class":30},"RouterView",[24,9445,9063],{"class":34},[24,9447,9448,9451,9453],{"class":26,"line":78},[24,9449,9450],{"class":34},"\u003C\u002F",[24,9452,9433],{"class":30},[24,9454,6417],{"class":34},[2795,9456,9458],{"id":9457},"step-6-add-ui-components-with-shadcn-vue","Step 6: Add UI Components with shadcn-vue",[10,9460,9461],{},"To avoid reinventing UI components, let's add shadcn-vue:",[14,9463,9465],{"className":1330,"code":9464,"language":1332,"meta":19,"style":19},"bunx --bun shadcn-vue@latest init\n",[21,9466,9467],{"__ignoreMap":19},[24,9468,9469,9471,9474,9477],{"class":26,"line":27},[24,9470,5665],{"class":389},[24,9472,9473],{"class":3231}," --bun",[24,9475,9476],{"class":350}," shadcn-vue@latest",[24,9478,3366],{"class":350},[10,9480,9481],{},"When prompted, choose your preferred color scheme (I usually go with Neutral).",[10,9483,9484],{},"Now let's add a button component:",[14,9486,9488],{"className":1330,"code":9487,"language":1332,"meta":19,"style":19},"bunx --bun shadcn-vue@latest add button\n",[21,9489,9490],{"__ignoreMap":19},[24,9491,9492,9494,9496,9498,9500],{"class":26,"line":27},[24,9493,5665],{"class":389},[24,9495,9473],{"class":3231},[24,9497,9476],{"class":350},[24,9499,1432],{"class":350},[24,9501,9502],{"class":350}," button\n",[10,9504,9505,9506,2952],{},"Create a page file at ",[21,9507,9508],{},"src\u002Fpages\u002Findex.vue",[14,9510,9512],{"className":9423,"code":9511,"language":8450,"meta":19,"style":19},"\u003Cscript setup lang=\"ts\">\nimport { Button } from \"@\u002Fcomponents\u002Fui\u002Fbutton\";\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"grid h-dvh place-items-center\">\n    \u003CButton>Clean Setup Complete!\u003C\u002FButton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[21,9513,9514,9537,9558,9566,9570,9578,9599,9618,9627],{"__ignoreMap":19},[24,9515,9516,9518,9521,9524,9527,9529,9531,9533,9535],{"class":26,"line":27},[24,9517,6208],{"class":34},[24,9519,9520],{"class":30},"script",[24,9522,9523],{"class":44}," setup",[24,9525,9526],{"class":44}," lang",[24,9528,6407],{"class":34},[24,9530,4566],{"class":346},[24,9532,18],{"class":350},[24,9534,4566],{"class":346},[24,9536,6417],{"class":34},[24,9538,9539,9541,9543,9545,9547,9549,9551,9554,9556],{"class":26,"line":57},[24,9540,31],{"class":30},[24,9542,334],{"class":34},[24,9544,6622],{"class":44},[24,9546,340],{"class":34},[24,9548,343],{"class":30},[24,9550,3287],{"class":346},[24,9552,9553],{"class":350},"@\u002Fcomponents\u002Fui\u002Fbutton",[24,9555,4566],{"class":346},[24,9557,4569],{"class":34},[24,9559,9560,9562,9564],{"class":26,"line":78},[24,9561,9450],{"class":34},[24,9563,9520],{"class":30},[24,9565,6417],{"class":34},[24,9567,9568],{"class":26,"line":226},[24,9569,379],{"emptyLinePlaceholder":378},[24,9571,9572,9574,9576],{"class":26,"line":238},[24,9573,6208],{"class":34},[24,9575,9433],{"class":30},[24,9577,6417],{"class":34},[24,9579,9580,9582,9585,9588,9590,9592,9595,9597],{"class":26,"line":244},[24,9581,9440],{"class":34},[24,9583,9584],{"class":30},"div",[24,9586,9587],{"class":44}," class",[24,9589,6407],{"class":34},[24,9591,4566],{"class":346},[24,9593,9594],{"class":350},"grid h-dvh place-items-center",[24,9596,4566],{"class":346},[24,9598,6417],{"class":34},[24,9600,9601,9603,9606,9609,9612,9614,9616],{"class":26,"line":403},[24,9602,6360],{"class":34},[24,9604,9605],{"class":30},"Button",[24,9607,9608],{"class":34},">",[24,9610,9611],{"class":7248},"Clean Setup Complete!",[24,9613,9450],{"class":34},[24,9615,9605],{"class":30},[24,9617,6417],{"class":34},[24,9619,9620,9623,9625],{"class":26,"line":422},[24,9621,9622],{"class":34},"  \u003C\u002F",[24,9624,9584],{"class":30},[24,9626,6417],{"class":34},[24,9628,9629,9631,9633],{"class":26,"line":453},[24,9630,9450],{"class":34},[24,9632,9433],{"class":30},[24,9634,6417],{"class":34},[108,9636,9638],{"id":9637},"what-this-gets-you","What This Gets You",[2795,9640,9642],{"id":9641},"typescript-that-works","TypeScript That Works",[10,9644,9645],{},"With proper configuration and unplugin-vue-router:",[1402,9647,9648,9654,9657],{},[1405,9649,9650,9651,2775],{},"Fully typed routes (try ",[21,9652,9653],{},"useRoute(\"\u002Fusers\u002F[id]\")",[1405,9655,9656],{},"Type checking on your components",[1405,9658,9659],{},"Auto-completion everywhere it matters",[2795,9661,9663],{"id":9662},"file-based-routing","File-Based Routing",[10,9665,7962,9666,9669,9670,9673,9674,9677,9678,9681],{},[21,9667,9668],{},"src\u002Fpages\u002Fabout.vue"," and it's available at ",[21,9671,9672],{},"\u002Fabout",". Create ",[21,9675,9676],{},"src\u002Fpages\u002Fusers\u002F[id].vue"," for a dynamic route at ",[21,9679,9680],{},"\u002Fusers\u002F:id",". All typed:",[14,9683,9685],{"className":9423,"code":9684,"language":8450,"meta":19,"style":19},"\u003Cscript setup lang=\"ts\">\nimport { useRoute } from \"vue-router\";\n\n\u002F\u002F This will be perfectly typed, with route.params.id as a string!\nconst route = useRoute(\"\u002Fusers\u002F[id]\");\n\u003C\u002Fscript>\n",[21,9686,9687,9707,9728,9732,9737,9759],{"__ignoreMap":19},[24,9688,9689,9691,9693,9695,9697,9699,9701,9703,9705],{"class":26,"line":27},[24,9690,6208],{"class":34},[24,9692,9520],{"class":30},[24,9694,9523],{"class":44},[24,9696,9526],{"class":44},[24,9698,6407],{"class":34},[24,9700,4566],{"class":346},[24,9702,18],{"class":350},[24,9704,4566],{"class":346},[24,9706,6417],{"class":34},[24,9708,9709,9711,9713,9716,9718,9720,9722,9724,9726],{"class":26,"line":57},[24,9710,31],{"class":30},[24,9712,334],{"class":34},[24,9714,9715],{"class":44}," useRoute",[24,9717,340],{"class":34},[24,9719,343],{"class":30},[24,9721,3287],{"class":346},[24,9723,9116],{"class":350},[24,9725,4566],{"class":346},[24,9727,4569],{"class":34},[24,9729,9730],{"class":26,"line":78},[24,9731,379],{"emptyLinePlaceholder":378},[24,9733,9734],{"class":26,"line":226},[24,9735,9736],{"class":53},"\u002F\u002F This will be perfectly typed, with route.params.id as a string!\n",[24,9738,9739,9741,9744,9746,9748,9750,9752,9755,9757],{"class":26,"line":238},[24,9740,9361],{"class":151},[24,9742,9743],{"class":44},"route",[24,9745,158],{"class":34},[24,9747,9715],{"class":389},[24,9749,443],{"class":34},[24,9751,4566],{"class":346},[24,9753,9754],{"class":350},"\u002Fusers\u002F[id]",[24,9756,4566],{"class":346},[24,9758,6717],{"class":34},[24,9760,9761,9763,9765],{"class":26,"line":244},[24,9762,9450],{"class":34},[24,9764,9520],{"class":30},[24,9766,6417],{"class":34},[2795,9768,9770],{"id":9769},"ui-components","UI Components",[10,9772,9773],{},"shadcn-vue provides:",[1402,9775,9776,9779,9782,9785],{},[1405,9777,9778],{},"Accessible components out of the box",[1405,9780,9781],{},"Consistent styling with Tailwind",[1405,9783,9784],{},"Customizable design tokens",[1405,9786,9787],{},"Only the components you need (reducing bundle size)",[2795,9789,9791],{"id":9790},"performance","Performance",[10,9793,9794],{},"Performance tracking in dev, stripped in production:",[14,9796,9798],{"className":4543,"code":9797,"language":4545,"meta":19,"style":19},"import { createApp } from \"vue\";\n\nimport App from \"@\u002FApp.vue\";\nimport router from \"@\u002Frouter\";\nimport \"@\u002Fstyle.css\";\n\nconst app = createApp(App);\n\napp.config.performance = import.meta.env.DEV;\n\napp.config.compilerOptions = {\n  comments: false,\n  whitespace: \"condense\",\n};\n\nif (import.meta.env.PROD) {\n  app.config.warnHandler = () => null;\n}\n\napp.use(router);\napp.mount(\"#app\");\n",[21,9799,9800,9820,9824,9840,9856,9868,9872,9888,9892,9925,9929,9944,9955,9971,9976,9980,10006,10032,10036,10040,10054],{"__ignoreMap":19},[24,9801,9802,9804,9806,9808,9810,9812,9814,9816,9818],{"class":26,"line":27},[24,9803,31],{"class":30},[24,9805,334],{"class":34},[24,9807,9288],{"class":44},[24,9809,340],{"class":34},[24,9811,343],{"class":30},[24,9813,3287],{"class":346},[24,9815,8450],{"class":350},[24,9817,4566],{"class":346},[24,9819,4569],{"class":34},[24,9821,9822],{"class":26,"line":57},[24,9823,379],{"emptyLinePlaceholder":378},[24,9825,9826,9828,9830,9832,9834,9836,9838],{"class":26,"line":78},[24,9827,31],{"class":30},[24,9829,9311],{"class":44},[24,9831,343],{"class":30},[24,9833,3287],{"class":346},[24,9835,9318],{"class":350},[24,9837,4566],{"class":346},[24,9839,4569],{"class":34},[24,9841,9842,9844,9846,9848,9850,9852,9854],{"class":26,"line":226},[24,9843,31],{"class":30},[24,9845,8677],{"class":44},[24,9847,343],{"class":30},[24,9849,3287],{"class":346},[24,9851,9335],{"class":350},[24,9853,4566],{"class":346},[24,9855,4569],{"class":34},[24,9857,9858,9860,9862,9864,9866],{"class":26,"line":238},[24,9859,31],{"class":30},[24,9861,3287],{"class":346},[24,9863,9348],{"class":350},[24,9865,4566],{"class":346},[24,9867,4569],{"class":34},[24,9869,9870],{"class":26,"line":244},[24,9871,379],{"emptyLinePlaceholder":378},[24,9873,9874,9876,9878,9880,9882,9884,9886],{"class":26,"line":403},[24,9875,9361],{"class":151},[24,9877,9364],{"class":44},[24,9879,158],{"class":34},[24,9881,9288],{"class":389},[24,9883,443],{"class":34},[24,9885,9373],{"class":44},[24,9887,6717],{"class":34},[24,9889,9890],{"class":26,"line":422},[24,9891,379],{"emptyLinePlaceholder":378},[24,9893,9894,9896,9898,9901,9903,9905,9907,9910,9912,9914,9916,9918,9920,9923],{"class":26,"line":453},[24,9895,9364],{"class":44},[24,9897,35],{"class":34},[24,9899,9900],{"class":44},"config",[24,9902,35],{"class":34},[24,9904,9790],{"class":44},[24,9906,158],{"class":34},[24,9908,9909],{"class":30}," import",[24,9911,35],{"class":34},[24,9913,39],{"class":38},[24,9915,35],{"class":34},[24,9917,45],{"class":44},[24,9919,35],{"class":34},[24,9921,9922],{"class":44},"DEV",[24,9924,4569],{"class":34},[24,9926,9927],{"class":26,"line":490},[24,9928,379],{"emptyLinePlaceholder":378},[24,9930,9931,9933,9935,9937,9939,9941],{"class":26,"line":496},[24,9932,9364],{"class":44},[24,9934,35],{"class":34},[24,9936,9900],{"class":44},[24,9938,35],{"class":34},[24,9940,8893],{"class":44},[24,9942,9943],{"class":34}," = {\n",[24,9945,9946,9949,9951,9953],{"class":26,"line":504},[24,9947,9948],{"class":38},"  comments",[24,9950,220],{"class":34},[24,9952,566],{"class":30},[24,9954,4637],{"class":34},[24,9956,9957,9960,9962,9964,9967,9969],{"class":26,"line":520},[24,9958,9959],{"class":38},"  whitespace",[24,9961,220],{"class":34},[24,9963,4566],{"class":346},[24,9965,9966],{"class":350},"condense",[24,9968,4566],{"class":346},[24,9970,4637],{"class":34},[24,9972,9973],{"class":26,"line":545},[24,9974,9975],{"class":34},"};\n",[24,9977,9978],{"class":26,"line":571},[24,9979,379],{"emptyLinePlaceholder":378},[24,9981,9982,9985,9987,9989,9991,9993,9995,9997,9999,10002,10004],{"class":26,"line":640},[24,9983,9984],{"class":30},"if",[24,9986,3995],{"class":34},[24,9988,31],{"class":30},[24,9990,35],{"class":34},[24,9992,39],{"class":38},[24,9994,35],{"class":34},[24,9996,45],{"class":44},[24,9998,35],{"class":34},[24,10000,10001],{"class":44},"PROD",[24,10003,2775],{"class":34},[24,10005,209],{"class":34},[24,10007,10008,10011,10013,10015,10017,10020,10022,10025,10027,10030],{"class":26,"line":645},[24,10009,10010],{"class":44},"  app",[24,10012,35],{"class":34},[24,10014,9900],{"class":44},[24,10016,35],{"class":34},[24,10018,10019],{"class":389},"warnHandler",[24,10021,158],{"class":34},[24,10023,10024],{"class":34}," ()",[24,10026,6691],{"class":34},[24,10028,10029],{"class":151}," null",[24,10031,4569],{"class":34},[24,10033,10034],{"class":26,"line":5039},[24,10035,247],{"class":34},[24,10037,10038],{"class":26,"line":5045},[24,10039,379],{"emptyLinePlaceholder":378},[24,10041,10042,10044,10046,10048,10050,10052],{"class":26,"line":5051},[24,10043,9364],{"class":44},[24,10045,35],{"class":34},[24,10047,9388],{"class":389},[24,10049,443],{"class":34},[24,10051,9155],{"class":44},[24,10053,6717],{"class":34},[24,10055,10056,10058,10060,10062,10064,10066,10068,10070],{"class":26,"line":5056},[24,10057,9364],{"class":44},[24,10059,35],{"class":34},[24,10061,9403],{"class":389},[24,10063,443],{"class":34},[24,10065,4566],{"class":346},[24,10067,9410],{"class":350},[24,10069,4566],{"class":346},[24,10071,6717],{"class":34},[108,10073,10075],{"id":10074},"results","Results",[10,10077,10078],{},"Used across multiple client projects:",[1402,10080,10081,10087,10093,10098],{},[1405,10082,10083,10086],{},[265,10084,10085],{},"Development speed",": New features take ~30% less time to implement",[1405,10088,10089,10092],{},[265,10090,10091],{},"Bundle size",": ~20% smaller than my previous setups",[1405,10094,10095,10097],{},[265,10096,9791],{},": Consistently scoring 95+ on Lighthouse",[1405,10099,10100,10103],{},[265,10101,10102],{},"Maintenance",": Much easier to onboard new developers",[108,10105,10107],{"id":10106},"when-to-use-this","When to Use This",[10,10109,10110],{},"Works well if you want:",[1402,10112,10113,10116,10119,10122,10125],{},[1405,10114,10115],{},"A lightweight Vue setup without Nuxt's overhead",[1405,10117,10118],{},"Type safety without the complexity",[1405,10120,10121],{},"Modern file-based routing",[1405,10123,10124],{},"Production-ready performance optimizations",[1405,10126,10127],{},"A component library that won't slow you down",[10,10129,10130],{},"Skip it if:",[1402,10132,10133,10136,10139],{},[1405,10134,10135],{},"You need SSR\u002FSSG (use Nuxt)",[1405,10137,10138],{},"You're working with an existing project with different conventions",[1405,10140,10141],{},"You prefer a different UI approach than Tailwind",[10,10143,10144,10145,10150],{},"For most client projects, this hits the right balance. The ",[268,10146,10149],{"href":10147,"rel":10148},"https:\u002F\u002Fgithub.com\u002Fpyyupsk\u002Fvue-setup",[272],"template repository"," is available to clone.",[1443,10152,10153],{},"html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .sRmUx, html code.shiki .sRmUx{--shiki-default:#99841877;--shiki-dark:#B8A96577}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}",{"title":19,"searchDepth":57,"depth":57,"links":10155},[10156,10157,10165,10171,10172],{"id":7604,"depth":57,"text":7605},{"id":7653,"depth":57,"text":7654,"children":10158},[10159,10160,10161,10162,10163,10164],{"id":7657,"depth":78,"text":7658},{"id":7708,"depth":78,"text":7709},{"id":7921,"depth":78,"text":7922},{"id":8325,"depth":78,"text":8326},{"id":8594,"depth":78,"text":8595},{"id":9457,"depth":78,"text":9458},{"id":9637,"depth":57,"text":9638,"children":10166},[10167,10168,10169,10170],{"id":9641,"depth":78,"text":9642},{"id":9662,"depth":78,"text":9663},{"id":9769,"depth":78,"text":9770},{"id":9790,"depth":78,"text":9791},{"id":10074,"depth":57,"text":10075},{"id":10106,"depth":57,"text":10107},"2025-04-27","My Vue starter after years of client projects — TypeScript, Vite, file-based routing, done in minutes.",{},"\u002Fwritings\u002Ftired-of-vue-boilerplate-heres-my-clean-fast-setup",{"title":7596,"description":10174},"writings\u002Ftired-of-vue-boilerplate-heres-my-clean-fast-setup","ptmDxvEnkuFNEUVM3g7NxCTtj0s4kGxa1KbTl9JZTjQ",{"id":10181,"title":10182,"body":10183,"category":4436,"date":11868,"description":11869,"draft":1462,"extension":1463,"meta":11870,"navigation":378,"path":11871,"seo":11872,"sitemap":378,"stem":11873,"__hash__":11874},"writings\u002Fwritings\u002Fthe-real-vue-js-developer-journey-wins-woes-and-lessons.md","The Real Vue.js Developer Journey: Wins, Woes & Lessons",{"type":7,"value":10184,"toc":11849},[10185,10210,10213,10217,10220,10246,10249,10253,10269,10284,10288,10291,10295,10301,10304,10957,10960,10986,10989,10993,10996,11002,11006,11434,11437,11444,11446,11449,11452,11480,11758,11761,11765,11768,11771,11775,11779,11804,11808,11838,11840,11843,11846],[10,10186,10187,10188,10192,10193,3745,10196,10199,10200,3879,10203,3745,10206,10209],{},"Transitioning from React to Vue.js was disorienting at first. The concepts were familiar — component-based architecture, declarative UI — but ",[10189,10190,10191],"em",{},"just different enough"," to throw me off. ",[21,10194,10195],{},"v-for",[21,10197,10198],{},"map()","? ",[21,10201,10202],{},"ref()",[21,10204,10205],{},"reactive()",[21,10207,10208],{},"useState()","? It took time to adjust.",[10,10211,10212],{},"Once I pushed through that friction, Vue clicked. The more I built with it — prototyping MVPs, scaling client apps — the more the design choices made sense. It became a go-to for projects that needed fast iteration without sacrificing maintainability.",[108,10214,10216],{"id":10215},"my-vue-setup","My Vue Setup",[10,10218,10219],{},"My Vue 3 stack, used across multiple client projects:",[1402,10221,10222,10228,10234,10240],{},[1405,10223,10224,10227],{},[265,10225,10226],{},"Vite"," — Fast dev server with HMR that keeps iteration cycles under seconds.",[1405,10229,10230,10233],{},[265,10231,10232],{},"Pinia"," — State management with a modular, composable design. Scales without the bloat.",[1405,10235,10236,10239],{},[265,10237,10238],{},"Vue Router"," — Flexible client-side navigation. Works well with auth flows and lazy loading.",[1405,10241,10242,10245],{},[265,10243,10244],{},"TypeScript"," — Type safety that catches bugs before runtime and keeps codebases maintainable.",[10,10247,10248],{},"This combination is the result of evaluating trade-offs between build speed and feature richness. It has its quirks — here's what I've learned working through them.",[108,10250,10252],{"id":10251},"the-learning-curve","The Learning Curve",[10,10254,10255,10256,10258,10259,10261,10262,3879,10265,10268],{},"Coming from React, I had to unlearn a lot. Vue's reactivity is proxy-based — ",[21,10257,10202],{}," for single values, ",[21,10260,10205],{}," for objects, ",[21,10263,10264],{},"watch()",[21,10266,10267],{},"watchEffect()"," for side effects. Unlike React's explicit re-renders via state setters, Vue's fine-grained reactivity updates only what's necessary. That leads to performance gains, but requires understanding to avoid pitfalls like over-reactivity.",[10,10270,10271,10272,10274,10275,597,10277,10274,10280,10283],{},"I initially tried mapping everything to React equivalents — ",[21,10273,10202],{}," as ",[21,10276,10208],{},[21,10278,10279],{},"computed()",[21,10281,10282],{},"useMemo()",". That didn't work well. Vue has its own philosophy: declarative rendering, composition over inheritance, and a reactivity model designed for data-driven UIs.",[2795,10285,10287],{"id":10286},"what-helped","What Helped",[10,10289,10290],{},"Building things. Vue's documentation is clear and packed with interactive examples. Small side projects — a dashboard POC, a few component experiments — helped me test ideas without client deadlines. Starting with the Composition API and building small components before tackling larger features made the adjustment manageable.",[108,10292,10294],{"id":10293},"state-management-with-pinia","State Management with Pinia",[10,10296,10297,10298,10300],{},"I'd heard about Vuex's mutation-heavy boilerplate and wasn't looking forward to it. Then I found ",[265,10299,10232],{}," — Vuex reimagined: lighter, more intuitive, and designed for the Composition API.",[10,10302,10303],{},"Pinia offers a flatter structure, better dev tools integration (time-travel debugging in Vue Devtools), and solid TypeScript support. A cart store from a recent e-commerce project:",[14,10305,10307],{"className":4543,"code":10306,"language":4545,"meta":19,"style":19},"import { defineStore } from \"pinia\";\n\nexport const useCartStore = defineStore(\"cart\", {\n  state: () => ({\n    items: [],\n    loading: false,\n    error: null,\n  }),\n\n  getters: {\n    itemCount: (state) => state.items.reduce((sum, i) => sum + i.quantity, 0),\n    totalPrice: (state) =>\n      state.items.reduce((sum, i) => sum + i.price * i.quantity, 0),\n    discountedTotal: (state) => {\n      const total = state.items.reduce(\n        (sum, i) => sum + i.price * i.quantity,\n        0,\n      );\n      return total > 100 ? total * 0.9 : total; \u002F\u002F 10% discount over $100\n    },\n  },\n\n  actions: {\n    async fetchCart(userId) {\n      try {\n        this.loading = true;\n        this.items = await fetchCartFromAPI(userId);\n      } catch (err) {\n        this.error =\n          err instanceof Error ? err.message : \"Oops, something went wrong.\";\n      } finally {\n        this.loading = false;\n      }\n    },\n    addItem(item) {\n      const existing = this.items.find((i) => i.id === item.id);\n      if (existing) {\n        existing.quantity += item.quantity;\n      } else {\n        this.items.push(item);\n      }\n    },\n  },\n});\n",[21,10308,10309,10331,10335,10361,10369,10377,10388,10400,10405,10409,10416,10473,10485,10533,10545,10568,10605,10612,10617,10652,10656,10660,10664,10671,10686,10693,10710,10732,10747,10758,10790,10799,10813,10818,10822,10834,10881,10892,10913,10922,10941,10945,10949,10953],{"__ignoreMap":19},[24,10310,10311,10313,10315,10318,10320,10322,10324,10327,10329],{"class":26,"line":27},[24,10312,31],{"class":30},[24,10314,334],{"class":34},[24,10316,10317],{"class":44}," defineStore",[24,10319,340],{"class":34},[24,10321,343],{"class":30},[24,10323,3287],{"class":346},[24,10325,10326],{"class":350},"pinia",[24,10328,4566],{"class":346},[24,10330,4569],{"class":34},[24,10332,10333],{"class":26,"line":57},[24,10334,379],{"emptyLinePlaceholder":378},[24,10336,10337,10339,10341,10344,10346,10348,10350,10352,10355,10357,10359],{"class":26,"line":78},[24,10338,148],{"class":30},[24,10340,152],{"class":151},[24,10342,10343],{"class":44},"useCartStore",[24,10345,158],{"class":34},[24,10347,10317],{"class":389},[24,10349,443],{"class":34},[24,10351,4566],{"class":346},[24,10353,10354],{"class":350},"cart",[24,10356,4566],{"class":346},[24,10358,6730],{"class":34},[24,10360,209],{"class":34},[24,10362,10363,10366],{"class":26,"line":226},[24,10364,10365],{"class":389},"  state",[24,10367,10368],{"class":34},": () => ({\n",[24,10370,10371,10374],{"class":26,"line":238},[24,10372,10373],{"class":38},"    items",[24,10375,10376],{"class":34},": [],\n",[24,10378,10379,10382,10384,10386],{"class":26,"line":244},[24,10380,10381],{"class":38},"    loading",[24,10383,220],{"class":34},[24,10385,566],{"class":30},[24,10387,4637],{"class":34},[24,10389,10390,10393,10395,10398],{"class":26,"line":403},[24,10391,10392],{"class":38},"    error",[24,10394,220],{"class":34},[24,10396,10397],{"class":151},"null",[24,10399,4637],{"class":34},[24,10401,10402],{"class":26,"line":422},[24,10403,10404],{"class":34},"  }),\n",[24,10406,10407],{"class":26,"line":453},[24,10408,379],{"emptyLinePlaceholder":378},[24,10410,10411,10414],{"class":26,"line":490},[24,10412,10413],{"class":38},"  getters",[24,10415,400],{"class":34},[24,10417,10418,10421,10424,10426,10429,10431,10433,10436,10438,10441,10444,10447,10449,10452,10454,10456,10459,10462,10464,10467,10469,10471],{"class":26,"line":496},[24,10419,10420],{"class":389},"    itemCount",[24,10422,10423],{"class":34},": (",[24,10425,6727],{"class":44},[24,10427,10428],{"class":34},") => ",[24,10430,6727],{"class":44},[24,10432,35],{"class":34},[24,10434,10435],{"class":44},"items",[24,10437,35],{"class":34},[24,10439,10440],{"class":389},"reduce",[24,10442,10443],{"class":34},"((",[24,10445,10446],{"class":44},"sum",[24,10448,597],{"class":34},[24,10450,10451],{"class":44},"i",[24,10453,10428],{"class":34},[24,10455,10446],{"class":44},[24,10457,10458],{"class":151}," +",[24,10460,10461],{"class":44}," i",[24,10463,35],{"class":34},[24,10465,10466],{"class":44},"quantity",[24,10468,597],{"class":34},[24,10470,9242],{"class":446},[24,10472,450],{"class":34},[24,10474,10475,10478,10480,10482],{"class":26,"line":504},[24,10476,10477],{"class":389},"    totalPrice",[24,10479,10423],{"class":34},[24,10481,6727],{"class":44},[24,10483,10484],{"class":34},") =>\n",[24,10486,10487,10490,10492,10494,10496,10498,10500,10502,10504,10506,10508,10510,10512,10514,10516,10519,10521,10523,10525,10527,10529,10531],{"class":26,"line":520},[24,10488,10489],{"class":44},"      state",[24,10491,35],{"class":34},[24,10493,10435],{"class":44},[24,10495,35],{"class":34},[24,10497,10440],{"class":389},[24,10499,10443],{"class":34},[24,10501,10446],{"class":44},[24,10503,597],{"class":34},[24,10505,10451],{"class":44},[24,10507,10428],{"class":34},[24,10509,10446],{"class":44},[24,10511,10458],{"class":151},[24,10513,10461],{"class":44},[24,10515,35],{"class":34},[24,10517,10518],{"class":44},"price",[24,10520,956],{"class":151},[24,10522,10461],{"class":44},[24,10524,35],{"class":34},[24,10526,10466],{"class":44},[24,10528,597],{"class":34},[24,10530,9242],{"class":446},[24,10532,450],{"class":34},[24,10534,10535,10538,10540,10542],{"class":26,"line":545},[24,10536,10537],{"class":389},"    discountedTotal",[24,10539,10423],{"class":34},[24,10541,6727],{"class":44},[24,10543,10544],{"class":34},") => {\n",[24,10546,10547,10550,10553,10555,10558,10560,10562,10564,10566],{"class":26,"line":571},[24,10548,10549],{"class":151},"      const ",[24,10551,10552],{"class":44},"total",[24,10554,158],{"class":34},[24,10556,10557],{"class":44}," state",[24,10559,35],{"class":34},[24,10561,10435],{"class":44},[24,10563,35],{"class":34},[24,10565,10440],{"class":389},[24,10567,6251],{"class":34},[24,10569,10570,10573,10575,10577,10579,10581,10583,10586,10588,10590,10592,10594,10597,10599,10601,10603],{"class":26,"line":640},[24,10571,10572],{"class":34},"        (",[24,10574,10446],{"class":44},[24,10576,6730],{"class":34},[24,10578,10461],{"class":44},[24,10580,2775],{"class":34},[24,10582,6691],{"class":34},[24,10584,10585],{"class":44}," sum",[24,10587,6013],{"class":151},[24,10589,10451],{"class":44},[24,10591,35],{"class":34},[24,10593,10518],{"class":44},[24,10595,10596],{"class":151}," * ",[24,10598,10451],{"class":44},[24,10600,35],{"class":34},[24,10602,10466],{"class":44},[24,10604,4637],{"class":34},[24,10606,10607,10610],{"class":26,"line":645},[24,10608,10609],{"class":446},"        0",[24,10611,4637],{"class":34},[24,10613,10614],{"class":26,"line":5039},[24,10615,10616],{"class":34},"      );\n",[24,10618,10619,10622,10625,10628,10631,10634,10636,10638,10641,10644,10646,10649],{"class":26,"line":5045},[24,10620,10621],{"class":30},"      return",[24,10623,10624],{"class":44}," total",[24,10626,10627],{"class":34}," > ",[24,10629,10630],{"class":446},"100",[24,10632,10633],{"class":151}," ?",[24,10635,10624],{"class":44},[24,10637,956],{"class":151},[24,10639,10640],{"class":446}," 0.9",[24,10642,10643],{"class":151}," :",[24,10645,10624],{"class":44},[24,10647,10648],{"class":34},"; ",[24,10650,10651],{"class":53},"\u002F\u002F 10% discount over $100\n",[24,10653,10654],{"class":26,"line":5051},[24,10655,8090],{"class":34},[24,10657,10658],{"class":26,"line":5056},[24,10659,493],{"class":34},[24,10661,10662],{"class":26,"line":5061},[24,10663,379],{"emptyLinePlaceholder":378},[24,10665,10666,10669],{"class":26,"line":5067},[24,10667,10668],{"class":38},"  actions",[24,10670,400],{"class":34},[24,10672,10673,10676,10679,10681,10684],{"class":26,"line":5073},[24,10674,10675],{"class":151},"    async",[24,10677,10678],{"class":389}," fetchCart",[24,10680,443],{"class":34},[24,10682,10683],{"class":44},"userId",[24,10685,9220],{"class":34},[24,10687,10688,10691],{"class":26,"line":5236},[24,10689,10690],{"class":30},"      try",[24,10692,209],{"class":34},[24,10694,10695,10698,10700,10703,10706,10708],{"class":26,"line":5525},[24,10696,10697],{"class":3231},"        this",[24,10699,35],{"class":34},[24,10701,10702],{"class":44},"loading",[24,10704,10705],{"class":34}," = ",[24,10707,4634],{"class":30},[24,10709,4569],{"class":34},[24,10711,10712,10714,10716,10718,10720,10723,10726,10728,10730],{"class":26,"line":5531},[24,10713,10697],{"class":3231},[24,10715,35],{"class":34},[24,10717,10435],{"class":44},[24,10719,10705],{"class":34},[24,10721,10722],{"class":30},"await",[24,10724,10725],{"class":389}," fetchCartFromAPI",[24,10727,443],{"class":34},[24,10729,10683],{"class":44},[24,10731,6717],{"class":34},[24,10733,10734,10737,10740,10742,10745],{"class":26,"line":5537},[24,10735,10736],{"class":34},"      } ",[24,10738,10739],{"class":30},"catch",[24,10741,3995],{"class":34},[24,10743,10744],{"class":44},"err",[24,10746,9220],{"class":34},[24,10748,10749,10751,10753,10755],{"class":26,"line":5543},[24,10750,10697],{"class":3231},[24,10752,35],{"class":34},[24,10754,620],{"class":44},[24,10756,10757],{"class":34}," =\n",[24,10759,10760,10763,10766,10769,10771,10774,10776,10779,10781,10783,10786,10788],{"class":26,"line":5549},[24,10761,10762],{"class":44},"          err",[24,10764,10765],{"class":151}," instanceof",[24,10767,10768],{"class":205}," Error",[24,10770,10633],{"class":151},[24,10772,10773],{"class":44}," err",[24,10775,35],{"class":34},[24,10777,10778],{"class":44},"message",[24,10780,10643],{"class":151},[24,10782,3287],{"class":346},[24,10784,10785],{"class":350},"Oops, something went wrong.",[24,10787,4566],{"class":346},[24,10789,4569],{"class":34},[24,10791,10792,10794,10797],{"class":26,"line":8120},[24,10793,10736],{"class":34},[24,10795,10796],{"class":30},"finally",[24,10798,209],{"class":34},[24,10800,10801,10803,10805,10807,10809,10811],{"class":26,"line":8125},[24,10802,10697],{"class":3231},[24,10804,35],{"class":34},[24,10806,10702],{"class":44},[24,10808,10705],{"class":34},[24,10810,566],{"class":30},[24,10812,4569],{"class":34},[24,10814,10815],{"class":26,"line":8131},[24,10816,10817],{"class":34},"      }\n",[24,10819,10820],{"class":26,"line":8137},[24,10821,8090],{"class":34},[24,10823,10824,10827,10829,10832],{"class":26,"line":8143},[24,10825,10826],{"class":389},"    addItem",[24,10828,443],{"class":34},[24,10830,10831],{"class":44},"item",[24,10833,9220],{"class":34},[24,10835,10836,10838,10841,10843,10846,10848,10850,10852,10855,10857,10859,10861,10863,10865,10867,10870,10873,10875,10877,10879],{"class":26,"line":8149},[24,10837,10549],{"class":151},[24,10839,10840],{"class":44},"existing",[24,10842,158],{"class":34},[24,10844,10845],{"class":3231}," this",[24,10847,35],{"class":34},[24,10849,10435],{"class":44},[24,10851,35],{"class":34},[24,10853,10854],{"class":389},"find",[24,10856,10443],{"class":34},[24,10858,10451],{"class":44},[24,10860,2775],{"class":34},[24,10862,6691],{"class":34},[24,10864,10461],{"class":44},[24,10866,35],{"class":34},[24,10868,10869],{"class":44},"id",[24,10871,10872],{"class":151}," === ",[24,10874,10831],{"class":44},[24,10876,35],{"class":34},[24,10878,10869],{"class":44},[24,10880,6717],{"class":34},[24,10882,10883,10886,10888,10890],{"class":26,"line":8155},[24,10884,10885],{"class":30},"      if",[24,10887,3995],{"class":34},[24,10889,10840],{"class":44},[24,10891,9220],{"class":34},[24,10893,10894,10897,10899,10901,10904,10907,10909,10911],{"class":26,"line":8161},[24,10895,10896],{"class":44},"        existing",[24,10898,35],{"class":34},[24,10900,10466],{"class":44},[24,10902,10903],{"class":151}," +=",[24,10905,10906],{"class":44}," item",[24,10908,35],{"class":34},[24,10910,10466],{"class":44},[24,10912,4569],{"class":34},[24,10914,10915,10917,10920],{"class":26,"line":8167},[24,10916,10736],{"class":34},[24,10918,10919],{"class":30},"else",[24,10921,209],{"class":34},[24,10923,10924,10926,10928,10930,10932,10935,10937,10939],{"class":26,"line":8173},[24,10925,10697],{"class":3231},[24,10927,35],{"class":34},[24,10929,10435],{"class":44},[24,10931,35],{"class":34},[24,10933,10934],{"class":389},"push",[24,10936,443],{"class":34},[24,10938,10831],{"class":44},[24,10940,6717],{"class":34},[24,10942,10943],{"class":26,"line":8179},[24,10944,10817],{"class":34},[24,10946,10947],{"class":26,"line":8185},[24,10948,8090],{"class":34},[24,10950,10951],{"class":26,"line":8190},[24,10952,493],{"class":34},[24,10954,10955],{"class":26,"line":8196},[24,10956,4653],{"class":34},[10,10958,10959],{},"What works well:",[1402,10961,10962,10968,10974,10980],{},[1405,10963,10964,10967],{},[265,10965,10966],{},"TypeScript-friendly"," — inferred types, less manual declarations.",[1405,10969,10970,10973],{},[265,10971,10972],{},"Auto-complete in VS Code"," — faster development, fewer errors.",[1405,10975,10976,10979],{},[265,10977,10978],{},"No boilerplate mutations"," — actions handle sync\u002Fasync logic directly.",[1405,10981,10982,10985],{},[265,10983,10984],{},"Modular"," — plugins for persistence (localStorage sync) or undo\u002Fredo.",[10,10987,10988],{},"For larger apps, Pinia's store composition splits state across modules without losing organization.",[108,10990,10992],{"id":10991},"routing","Routing",[10,10994,10995],{},"Vue Router is powerful, but dynamic routes, nested views, and programmatic navigation took time to get comfortable with — especially coming from Next.js's file-based routing.",[10,10997,10998,10999,35],{},"The manual route definitions felt dated at first, but the flexibility pays off for role-based access control or dynamic params like ",[21,11000,11001],{},"\u002Fdashboard\u002F:userId\u002Fwidgets",[2795,11003,11005],{"id":11004},"my-typical-setup","My Typical Setup",[14,11007,11009],{"className":4543,"code":11008,"language":4545,"meta":19,"style":19},"import { createRouter, createWebHistory } from \"vue-router\";\n\nimport Dashboard from \"@\u002Fviews\u002FDashboard.vue\";\nimport Home from \"@\u002Fviews\u002FHome.vue\";\nimport Product from \"@\u002Fviews\u002FProduct.vue\";\n\nconst routes = [\n  { path: \"\u002F\", component: Home },\n  { path: \"\u002Fproduct\u002F:id\", component: Product, props: true },\n  {\n    path: \"\u002Fdashboard\u002F:userId\",\n    component: Dashboard,\n    meta: { requiresAuth: true },\n    children: [\n      { path: \"widgets\", component: () => import(\"@\u002Fcomponents\u002FWidgets.vue\") },\n    ],\n  },\n];\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes,\n});\n\nrouter.beforeEach((to, from, next) => {\n  if (to.meta.requiresAuth && !isAuthenticated()) {\n    next(\"\u002Flogin\");\n  } else {\n    next();\n  }\n});\n\nexport default router;\n",[21,11010,11011,11035,11039,11057,11075,11093,11097,11109,11138,11173,11177,11193,11205,11222,11229,11266,11271,11275,11280,11284,11296,11306,11312,11316,11320,11349,11380,11396,11406,11412,11416,11420,11424],{"__ignoreMap":19},[24,11012,11013,11015,11017,11019,11021,11023,11025,11027,11029,11031,11033],{"class":26,"line":27},[24,11014,31],{"class":30},[24,11016,334],{"class":34},[24,11018,9102],{"class":44},[24,11020,6730],{"class":34},[24,11022,9107],{"class":44},[24,11024,340],{"class":34},[24,11026,343],{"class":30},[24,11028,3287],{"class":346},[24,11030,9116],{"class":350},[24,11032,4566],{"class":346},[24,11034,4569],{"class":34},[24,11036,11037],{"class":26,"line":57},[24,11038,379],{"emptyLinePlaceholder":378},[24,11040,11041,11043,11046,11048,11050,11053,11055],{"class":26,"line":78},[24,11042,31],{"class":30},[24,11044,11045],{"class":44}," Dashboard",[24,11047,343],{"class":30},[24,11049,3287],{"class":346},[24,11051,11052],{"class":350},"@\u002Fviews\u002FDashboard.vue",[24,11054,4566],{"class":346},[24,11056,4569],{"class":34},[24,11058,11059,11061,11064,11066,11068,11071,11073],{"class":26,"line":226},[24,11060,31],{"class":30},[24,11062,11063],{"class":44}," Home",[24,11065,343],{"class":30},[24,11067,3287],{"class":346},[24,11069,11070],{"class":350},"@\u002Fviews\u002FHome.vue",[24,11072,4566],{"class":346},[24,11074,4569],{"class":34},[24,11076,11077,11079,11082,11084,11086,11089,11091],{"class":26,"line":238},[24,11078,31],{"class":30},[24,11080,11081],{"class":44}," Product",[24,11083,343],{"class":30},[24,11085,3287],{"class":346},[24,11087,11088],{"class":350},"@\u002Fviews\u002FProduct.vue",[24,11090,4566],{"class":346},[24,11092,4569],{"class":34},[24,11094,11095],{"class":26,"line":244},[24,11096,379],{"emptyLinePlaceholder":378},[24,11098,11099,11101,11104,11106],{"class":26,"line":403},[24,11100,9361],{"class":151},[24,11102,11103],{"class":44},"routes",[24,11105,158],{"class":34},[24,11107,11108],{"class":34}," [\n",[24,11110,11111,11114,11116,11118,11120,11123,11125,11127,11130,11132,11135],{"class":26,"line":422},[24,11112,11113],{"class":34},"  { ",[24,11115,8486],{"class":38},[24,11117,220],{"class":34},[24,11119,4566],{"class":346},[24,11121,11122],{"class":350},"\u002F",[24,11124,4566],{"class":346},[24,11126,597],{"class":34},[24,11128,11129],{"class":38},"component",[24,11131,220],{"class":34},[24,11133,11134],{"class":44},"Home",[24,11136,11137],{"class":34}," },\n",[24,11139,11140,11142,11144,11146,11148,11151,11153,11155,11157,11159,11162,11164,11167,11169,11171],{"class":26,"line":453},[24,11141,11113],{"class":34},[24,11143,8486],{"class":38},[24,11145,220],{"class":34},[24,11147,4566],{"class":346},[24,11149,11150],{"class":350},"\u002Fproduct\u002F:id",[24,11152,4566],{"class":346},[24,11154,597],{"class":34},[24,11156,11129],{"class":38},[24,11158,220],{"class":34},[24,11160,11161],{"class":44},"Product",[24,11163,597],{"class":34},[24,11165,11166],{"class":38},"props",[24,11168,220],{"class":34},[24,11170,4634],{"class":30},[24,11172,11137],{"class":34},[24,11174,11175],{"class":26,"line":490},[24,11176,8031],{"class":34},[24,11178,11179,11182,11184,11186,11189,11191],{"class":26,"line":496},[24,11180,11181],{"class":38},"    path",[24,11183,220],{"class":34},[24,11185,4566],{"class":346},[24,11187,11188],{"class":350},"\u002Fdashboard\u002F:userId",[24,11190,4566],{"class":346},[24,11192,4637],{"class":34},[24,11194,11195,11198,11200,11203],{"class":26,"line":504},[24,11196,11197],{"class":38},"    component",[24,11199,220],{"class":34},[24,11201,11202],{"class":44},"Dashboard",[24,11204,4637],{"class":34},[24,11206,11207,11210,11213,11216,11218,11220],{"class":26,"line":520},[24,11208,11209],{"class":38},"    meta",[24,11211,11212],{"class":34},": { ",[24,11214,11215],{"class":38},"requiresAuth",[24,11217,220],{"class":34},[24,11219,4634],{"class":30},[24,11221,11137],{"class":34},[24,11223,11224,11227],{"class":26,"line":545},[24,11225,11226],{"class":38},"    children",[24,11228,8729],{"class":34},[24,11230,11231,11234,11236,11238,11240,11243,11245,11247,11249,11252,11254,11256,11258,11261,11263],{"class":26,"line":571},[24,11232,11233],{"class":34},"      { ",[24,11235,8486],{"class":38},[24,11237,220],{"class":34},[24,11239,4566],{"class":346},[24,11241,11242],{"class":350},"widgets",[24,11244,4566],{"class":346},[24,11246,597],{"class":34},[24,11248,11129],{"class":389},[24,11250,11251],{"class":34},": () => ",[24,11253,31],{"class":151},[24,11255,443],{"class":34},[24,11257,4566],{"class":346},[24,11259,11260],{"class":350},"@\u002Fcomponents\u002FWidgets.vue",[24,11262,4566],{"class":346},[24,11264,11265],{"class":34},") },\n",[24,11267,11268],{"class":26,"line":640},[24,11269,11270],{"class":34},"    ],\n",[24,11272,11273],{"class":26,"line":645},[24,11274,493],{"class":34},[24,11276,11277],{"class":26,"line":5039},[24,11278,11279],{"class":34},"];\n",[24,11281,11282],{"class":26,"line":5045},[24,11283,379],{"emptyLinePlaceholder":378},[24,11285,11286,11288,11290,11292,11294],{"class":26,"line":5051},[24,11287,9361],{"class":151},[24,11289,9155],{"class":44},[24,11291,158],{"class":34},[24,11293,9102],{"class":389},[24,11295,392],{"class":34},[24,11297,11298,11300,11302,11304],{"class":26,"line":5056},[24,11299,9166],{"class":38},[24,11301,220],{"class":34},[24,11303,9171],{"class":389},[24,11305,419],{"class":34},[24,11307,11308,11310],{"class":26,"line":5061},[24,11309,9195],{"class":44},[24,11311,4637],{"class":34},[24,11313,11314],{"class":26,"line":5067},[24,11315,4653],{"class":34},[24,11317,11318],{"class":26,"line":5073},[24,11319,379],{"emptyLinePlaceholder":378},[24,11321,11322,11324,11326,11329,11331,11334,11336,11338,11340,11343,11345,11347],{"class":26,"line":5236},[24,11323,9155],{"class":44},[24,11325,35],{"class":34},[24,11327,11328],{"class":389},"beforeEach",[24,11330,10443],{"class":34},[24,11332,11333],{"class":44},"to",[24,11335,6730],{"class":34},[24,11337,343],{"class":44},[24,11339,6730],{"class":34},[24,11341,11342],{"class":44}," next",[24,11344,2775],{"class":34},[24,11346,6691],{"class":34},[24,11348,209],{"class":34},[24,11350,11351,11354,11356,11358,11360,11362,11364,11366,11369,11372,11375,11378],{"class":26,"line":5525},[24,11352,11353],{"class":30},"  if",[24,11355,3995],{"class":34},[24,11357,11333],{"class":44},[24,11359,35],{"class":34},[24,11361,39],{"class":44},[24,11363,35],{"class":34},[24,11365,11215],{"class":44},[24,11367,11368],{"class":151}," &&",[24,11370,11371],{"class":151}," !",[24,11373,11374],{"class":389},"isAuthenticated",[24,11376,11377],{"class":34},"())",[24,11379,209],{"class":34},[24,11381,11382,11385,11387,11389,11392,11394],{"class":26,"line":5531},[24,11383,11384],{"class":389},"    next",[24,11386,443],{"class":34},[24,11388,4566],{"class":346},[24,11390,11391],{"class":350},"\u002Flogin",[24,11393,4566],{"class":346},[24,11395,6717],{"class":34},[24,11397,11398,11401,11404],{"class":26,"line":5537},[24,11399,11400],{"class":34},"  }",[24,11402,11403],{"class":30}," else",[24,11405,209],{"class":34},[24,11407,11408,11410],{"class":26,"line":5543},[24,11409,11384],{"class":389},[24,11411,6232],{"class":34},[24,11413,11414],{"class":26,"line":5549},[24,11415,7025],{"class":34},[24,11417,11418],{"class":26,"line":8120},[24,11419,4653],{"class":34},[24,11421,11422],{"class":26,"line":8125},[24,11423,379],{"emptyLinePlaceholder":378},[24,11425,11426,11428,11430,11432],{"class":26,"line":8131},[24,11427,148],{"class":30},[24,11429,386],{"class":30},[24,11431,8677],{"class":44},[24,11433,4569],{"class":34},[10,11435,11436],{},"For faster prototyping, I'm looking at file-based routing via unplugin-vue-router. Nuxt's convention-over-configuration approach is worth borrowing.",[10,11438,11439],{},[268,11440,11443],{"href":11441,"rel":11442},"https:\u002F\u002Fuvr.esm.is\u002Fintroduction.html",[272],"Learn more about advanced routing",[108,11445,9791],{"id":9790},[10,11447,11448],{},"Scaling to large datasets — real-time analytics dashboards — exposed issues: slow renders, sluggish updates, memory leaks. Vue's virtual DOM is efficient by default, but production apps need proactive optimization.",[2795,11450,10287],{"id":11451},"what-helped-1",[1402,11453,11454,11463,11475],{},[1405,11455,11456,6020,11459,11462],{},[265,11457,11458],{},"Lazy loading",[21,11460,11461],{},"defineAsyncComponent"," to chunk bundles.",[1405,11464,11465,11474],{},[265,11466,11467,11470,11471],{},[21,11468,11469],{},"computed"," over ",[21,11472,11473],{},"watch"," for cached derivations.",[1405,11476,11477],{},[265,11478,11479],{},"Dev performance tracking:",[14,11481,11483],{"className":4543,"code":11482,"language":4545,"meta":19,"style":19},"import { createApp } from \"vue\";\n\nimport App from \"@\u002FApp.vue\";\n\nconst app = createApp(App);\n\napp.config.performance = import.meta.env.DEV;\n\napp.config.compilerOptions = {\n  comments: false,\n  delimiters: [\"${\", \"}\"],\n  whitespace: \"condense\",\n};\n\napp.config.errorHandler = (err, instance, info) => {\n  console.error(`Error: ${err.toString()}\\nInfo: ${info}`);\n};\n\napp.mount(\"#app\");\n",[21,11484,11485,11505,11509,11525,11529,11545,11549,11579,11583,11597,11607,11630,11644,11648,11652,11687,11732,11736,11740],{"__ignoreMap":19},[24,11486,11487,11489,11491,11493,11495,11497,11499,11501,11503],{"class":26,"line":27},[24,11488,31],{"class":30},[24,11490,334],{"class":34},[24,11492,9288],{"class":44},[24,11494,340],{"class":34},[24,11496,343],{"class":30},[24,11498,3287],{"class":346},[24,11500,8450],{"class":350},[24,11502,4566],{"class":346},[24,11504,4569],{"class":34},[24,11506,11507],{"class":26,"line":57},[24,11508,379],{"emptyLinePlaceholder":378},[24,11510,11511,11513,11515,11517,11519,11521,11523],{"class":26,"line":78},[24,11512,31],{"class":30},[24,11514,9311],{"class":44},[24,11516,343],{"class":30},[24,11518,3287],{"class":346},[24,11520,9318],{"class":350},[24,11522,4566],{"class":346},[24,11524,4569],{"class":34},[24,11526,11527],{"class":26,"line":226},[24,11528,379],{"emptyLinePlaceholder":378},[24,11530,11531,11533,11535,11537,11539,11541,11543],{"class":26,"line":238},[24,11532,9361],{"class":151},[24,11534,9364],{"class":44},[24,11536,158],{"class":34},[24,11538,9288],{"class":389},[24,11540,443],{"class":34},[24,11542,9373],{"class":44},[24,11544,6717],{"class":34},[24,11546,11547],{"class":26,"line":244},[24,11548,379],{"emptyLinePlaceholder":378},[24,11550,11551,11553,11555,11557,11559,11561,11563,11565,11567,11569,11571,11573,11575,11577],{"class":26,"line":403},[24,11552,9364],{"class":44},[24,11554,35],{"class":34},[24,11556,9900],{"class":44},[24,11558,35],{"class":34},[24,11560,9790],{"class":44},[24,11562,158],{"class":34},[24,11564,9909],{"class":30},[24,11566,35],{"class":34},[24,11568,39],{"class":38},[24,11570,35],{"class":34},[24,11572,45],{"class":44},[24,11574,35],{"class":34},[24,11576,9922],{"class":44},[24,11578,4569],{"class":34},[24,11580,11581],{"class":26,"line":422},[24,11582,379],{"emptyLinePlaceholder":378},[24,11584,11585,11587,11589,11591,11593,11595],{"class":26,"line":453},[24,11586,9364],{"class":44},[24,11588,35],{"class":34},[24,11590,9900],{"class":44},[24,11592,35],{"class":34},[24,11594,8893],{"class":44},[24,11596,9943],{"class":34},[24,11598,11599,11601,11603,11605],{"class":26,"line":490},[24,11600,9948],{"class":38},[24,11602,220],{"class":34},[24,11604,566],{"class":30},[24,11606,4637],{"class":34},[24,11608,11609,11612,11614,11616,11618,11620,11622,11624,11626,11628],{"class":26,"line":496},[24,11610,11611],{"class":38},"  delimiters",[24,11613,716],{"class":34},[24,11615,4566],{"class":346},[24,11617,6378],{"class":350},[24,11619,4566],{"class":346},[24,11621,597],{"class":34},[24,11623,4566],{"class":346},[24,11625,6388],{"class":350},[24,11627,4566],{"class":346},[24,11629,1208],{"class":34},[24,11631,11632,11634,11636,11638,11640,11642],{"class":26,"line":504},[24,11633,9959],{"class":38},[24,11635,220],{"class":34},[24,11637,4566],{"class":346},[24,11639,9966],{"class":350},[24,11641,4566],{"class":346},[24,11643,4637],{"class":34},[24,11645,11646],{"class":26,"line":520},[24,11647,9975],{"class":34},[24,11649,11650],{"class":26,"line":545},[24,11651,379],{"emptyLinePlaceholder":378},[24,11653,11654,11656,11658,11660,11662,11665,11667,11669,11671,11673,11676,11678,11681,11683,11685],{"class":26,"line":571},[24,11655,9364],{"class":44},[24,11657,35],{"class":34},[24,11659,9900],{"class":44},[24,11661,35],{"class":34},[24,11663,11664],{"class":389},"errorHandler",[24,11666,158],{"class":34},[24,11668,3995],{"class":34},[24,11670,10744],{"class":44},[24,11672,6730],{"class":34},[24,11674,11675],{"class":44}," instance",[24,11677,6730],{"class":34},[24,11679,11680],{"class":44}," info",[24,11682,2775],{"class":34},[24,11684,6691],{"class":34},[24,11686,209],{"class":34},[24,11688,11689,11692,11694,11696,11698,11700,11703,11705,11707,11709,11712,11714,11716,11719,11722,11724,11726,11728,11730],{"class":26,"line":640},[24,11690,11691],{"class":44},"  console",[24,11693,35],{"class":34},[24,11695,620],{"class":389},[24,11697,443],{"class":34},[24,11699,6372],{"class":346},[24,11701,11702],{"class":350},"Error: ",[24,11704,6378],{"class":30},[24,11706,10744],{"class":350},[24,11708,35],{"class":34},[24,11710,11711],{"class":389},"toString",[24,11713,6490],{"class":34},[24,11715,6388],{"class":30},[24,11717,11718],{"class":3231},"\\n",[24,11720,11721],{"class":350},"Info: ",[24,11723,6378],{"class":30},[24,11725,602],{"class":350},[24,11727,6388],{"class":30},[24,11729,6372],{"class":346},[24,11731,6717],{"class":34},[24,11733,11734],{"class":26,"line":645},[24,11735,9975],{"class":34},[24,11737,11738],{"class":26,"line":5039},[24,11739,379],{"emptyLinePlaceholder":378},[24,11741,11742,11744,11746,11748,11750,11752,11754,11756],{"class":26,"line":5045},[24,11743,9364],{"class":44},[24,11745,35],{"class":34},[24,11747,9403],{"class":389},[24,11749,443],{"class":34},[24,11751,4566],{"class":346},[24,11753,9410],{"class":350},[24,11755,4566],{"class":346},[24,11757,6717],{"class":34},[10,11759,11760],{},"One project saw a 40% improvement in Time to Interactive (TTI). Vue Devtools and Lighthouse audits help track these gains.",[108,11762,11764],{"id":11763},"ecosystem-integration","Ecosystem Integration",[10,11766,11767],{},"I've integrated Vue with Node\u002FExpress and Firebase backends, using Axios or TanStack Query for data fetching. Vitest for unit tests, Cypress for E2E. Deployment via Vercel or Netlify.",[10,11769,11770],{},"Vue's footprint (under 30KB gzipped) makes it practical for micro-frontends or embedded UIs.",[108,11772,11774],{"id":11773},"the-verdict","The Verdict",[2795,11776,11778],{"id":11777},"what-works","What Works",[1402,11780,11781,11787,11793,11798],{},[1405,11782,11783,11786],{},[265,11784,11785],{},"Single-file components"," promote encapsulation and reusability.",[1405,11788,11789,11792],{},[265,11790,11791],{},"Reactive system"," feels natural once learned — efficient, declarative UIs.",[1405,11794,11795,11797],{},[265,11796,10232],{}," balances simplicity with extensibility.",[1405,11799,11800,11803],{},[265,11801,11802],{},"Ecosystem synergy"," — Vite + TypeScript + Router is a solid toolchain.",[2795,11805,11807],{"id":11806},"what-doesnt","What Doesn't",[1402,11809,11810,11816,11826,11832],{},[1405,11811,11812,11815],{},[265,11813,11814],{},"Setup overhead"," — TypeScript and routing configuration can slow down initial prototyping.",[1405,11817,11818,11821,11822,11825],{},[265,11819,11820],{},"Pinia overkill for simple apps"," — sometimes basic ",[21,11823,11824],{},"ref"," composables are enough.",[1405,11827,11828,11831],{},[265,11829,11830],{},"Eager reactivity"," needs finesse in data-intensive scenarios.",[1405,11833,11834,11837],{},[265,11835,11836],{},"Options vs Composition API split"," can be confusing when learning.",[108,11839,3127],{"id":3126},[10,11841,11842],{},"Vue went from a curiosity to a reliable choice. The syntax adjustment was real, but the framework underneath is solid — fast development without the overhead of heavier alternatives.",[10,11844,11845],{},"I still use Next.js when SSR\u002FSEO is the priority. But for lightweight, reactive frontends — especially greenfield projects — Vue 3 works well. Start small, build something real, and see how it feels.",[1443,11847,11848],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .scnC2, html code.shiki .scnC2{--shiki-default:#B5695977;--shiki-dark:#C98A7D77}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":19,"searchDepth":57,"depth":57,"links":11850},[11851,11852,11855,11856,11859,11862,11863,11867],{"id":10215,"depth":57,"text":10216},{"id":10251,"depth":57,"text":10252,"children":11853},[11854],{"id":10286,"depth":78,"text":10287},{"id":10293,"depth":57,"text":10294},{"id":10991,"depth":57,"text":10992,"children":11857},[11858],{"id":11004,"depth":78,"text":11005},{"id":9790,"depth":57,"text":9791,"children":11860},[11861],{"id":11451,"depth":78,"text":10287},{"id":11763,"depth":57,"text":11764},{"id":11773,"depth":57,"text":11774,"children":11864},[11865,11866],{"id":11777,"depth":78,"text":11778},{"id":11806,"depth":78,"text":11807},{"id":3126,"depth":57,"text":3127},"2025-03-17","From React to Vue.js — the wins, the friction, and what both ecosystems taught me.",{},"\u002Fwritings\u002Fthe-real-vue-js-developer-journey-wins-woes-and-lessons",{"title":10182,"description":11869},"writings\u002Fthe-real-vue-js-developer-journey-wins-woes-and-lessons","xgLHCym_A6B5squpwXaIJI6qRkZgxan5Ru19dQoDGgU",1775838177086]