[{"data":1,"prerenderedAt":1654},["ShallowReactive",2],{"page-en-\u002Fposts\u002Fessential-frontend-automations":3},{"id":4,"title":5,"body":6,"date":1641,"description":1642,"duration":1643,"extension":1644,"meta":1645,"navigation":817,"path":1646,"robots":1647,"seo":1648,"sitemap":1649,"stem":1650,"tags":1651,"toc":817,"__hash__":1653},"content_en\u002Fposts\u002Fessential-frontend-automations.md","Essential frontend automations",{"type":7,"value":8,"toc":1626},"minimark",[9,17,24,29,33,36,40,43,59,63,66,71,82,88,102,116,119,146,150,163,166,170,178,238,252,263,266,286,289,292,295,299,302,311,333,345,357,361,364,377,400,406,410,413,416,425,429,444,453,462,465,786,899,906,1081,1084,1088,1091,1094,1109,1112,1352,1484,1491,1583,1612,1615,1619,1622],[10,11,13],"h1",{"id":12},"",[14,15],"binding",{"value":16},"title",[18,19],"posts-date-duration",{":duration":20,":date":21,"className":22},"duration","date",[23],"-mt-6",[25,26,28],"h2",{"id":27},"introduction","Introduction",[30,31,32],"p",{},"After years of frontend work I've tried lots of automation tools — some turned out to be more hype than useful, and some turned out to be gems hidden in the sand.",[30,34,35],{},"Today I'll share my favorite automations, which I consider useful (some of them even essential). I've thoroughly used and tested them in production, and some of them differ from the common pipeline.",[25,37,39],{"id":38},"disclaimer","Disclaimer",[30,41,42],{},"I intentionally don't mention any LLM automations for several reasons:",[44,45,46,50,53,56],"ul",{},[47,48,49],"li",{},"There are lots of LLM automations — enough for another 1–2 articles",[47,51,52],{},"I haven't had enough experience testing LLM automations in production",[47,54,55],{},"LLM automations keep appearing, changing, and evolving",[47,57,58],{},"Current automations can be used inside LLM agents, so they complement each other",[25,60,62],{"id":61},"automations","Automations",[30,64,65],{},"This article is structured from more common\u002Ffamous tools to less known ones. You can use the TOC on the left to jump to the area of frontend development you're interested in.",[67,68,70],"h3",{"id":69},"linting-and-formatting-eslint-eslint-stylistic","Linting and formatting = ESLint + ESLint Stylistic",[30,72,73,74,81],{},"The most obvious automation is of course ",[75,76,80],"a",{"href":77,"rel":78},"https:\u002F\u002Feslint.org\u002F",[79],"nofollow","ESLint",".",[83,84,85],"blockquote",{},[30,86,87],{},"Wait, where's Prettier?",[30,89,90,91,96,97,81],{},"Yes, there's no ",[75,92,95],{"href":93,"rel":94},"https:\u002F\u002Fprettier.io\u002F",[79],"Prettier",". In my opinion it is better to use ",[75,98,101],{"href":99,"rel":100},"https:\u002F\u002Feslint.style\u002F",[79],"ESLint Stylistic",[30,103,104,105,110,111,115],{},"You can read about this in ",[75,106,109],{"href":107,"rel":108},"https:\u002F\u002Fantfu.me\u002Fposts\u002Fwhy-not-prettier",[79],"Why I don't use Prettier",", but my general issue is: ",[112,113,114],"strong",{},"if you don't like Prettier's styling, you will fight it and work around it",". Some behavior can't be configured in a fine-grained way, and some can't be overridden at all. ESLint gives much more flexibility where it's needed without compromising useful features.",[30,117,118],{},"If you haven't tried this workflow, I would definitely recommend giving it a try.",[30,120,121,122,127,128,133,134,139,140,145],{},"Also take a look at ",[75,123,126],{"href":124,"rel":125},"https:\u002F\u002Foxc.rs\u002Fdocs\u002Fguide\u002Fusage\u002Flinter",[79],"Oxlint"," and ",[75,129,132],{"href":130,"rel":131},"https:\u002F\u002Foxc.rs\u002Fdocs\u002Fguide\u002Fusage\u002Fformatter",[79],"Oxfmt",". At the time this article was written, Oxlint doesn't support many ESLint rules (",[75,135,138],{"href":136,"rel":137},"https:\u002F\u002Fgithub.com\u002Foxc-project\u002Foxc\u002Fissues\u002F481",[79],"general",", ",[75,141,144],{"href":142,"rel":143},"https:\u002F\u002Fgithub.com\u002Foxc-project\u002Foxc\u002Fissues\u002F15761",[79],"vue","), but that might change soon.",[67,147,149],{"id":148},"testing-playwright-nightly-smokes","Testing = Playwright + nightly smokes",[30,151,152,153,156,157,162],{},"Testing has lots of different automations, but one I think is underestimated: ",[112,154,155],{},"E2E nightly smokes",". Smoke tests are a small set of E2E cases that cover the most popular flows in your product. If you want that product to be as stable as possible, run them every night. I find ",[75,158,161],{"href":159,"rel":160},"https:\u002F\u002Fplaywright.dev\u002F",[79],"Playwright"," the best for E2E at the moment — strong support from Microsoft, better readability than Cypress, and all the major integrations.",[30,164,165],{},"The coolest and most underrated upside is catching regressions in a common workflow before the next workday — often before anyone else notices. If nobody noticed, it \"never happened\". A clean nightly history is also a solid argument when you talk about reliability or promotion.",[67,167,169],{"id":168},"code-quality-knip-husky","Code quality = Knip + Husky",[30,171,172,177],{},[75,173,176],{"href":174,"rel":175},"https:\u002F\u002Fknip.dev\u002F",[79],"Knip"," lets you check your code for a dead one. The issue with Knip is that it needs extra configuration if you use auto-imports — those files look unused by default. A minimal fix is to mark them as entry points or ignore them:",[179,180,184],"pre",{"className":181,"code":182,"language":183,"meta":12,"style":12},"language-json shiki shiki-themes github-light github-dark","{\n  \"entry\": [\"src\u002Fmain.ts\", \"src\u002Fcomponents\u002F**\u002F*.vue\"],\n  \"ignore\": [\"src\u002Fauto-imports.d.ts\"]\n}\n","json",[185,186,187,196,218,232],"code",{"__ignoreMap":12},[188,189,192],"span",{"class":190,"line":191},"line",1,[188,193,195],{"class":194},"sVt8B","{\n",[188,197,199,203,206,210,212,215],{"class":190,"line":198},2,[188,200,202],{"class":201},"sj4cs","  \"entry\"",[188,204,205],{"class":194},": [",[188,207,209],{"class":208},"sZZnC","\"src\u002Fmain.ts\"",[188,211,139],{"class":194},[188,213,214],{"class":208},"\"src\u002Fcomponents\u002F**\u002F*.vue\"",[188,216,217],{"class":194},"],\n",[188,219,221,224,226,229],{"class":190,"line":220},3,[188,222,223],{"class":201},"  \"ignore\"",[188,225,205],{"class":194},[188,227,228],{"class":208},"\"src\u002Fauto-imports.d.ts\"",[188,230,231],{"class":194},"]\n",[188,233,235],{"class":190,"line":234},4,[188,236,237],{"class":194},"}\n",[30,239,240,245,246,251],{},[75,241,244],{"href":242,"rel":243},"https:\u002F\u002Ftypicode.github.io\u002Fhusky\u002F",[79],"Husky"," lets you run commands on git hooks — most often pre-commit. Common use cases are running ",[75,247,250],{"href":248,"rel":249},"https:\u002F\u002Fgithub.com\u002Flint-staged\u002Flint-staged",[79],"lint-staged"," so only staged files get linted, or checking that commit messages follow a convention.",[30,253,254,255,258,259,262],{},"That said, hot take — ",[112,256,257],{},"I wouldn't use Husky",", or at bare minimum ",[112,260,261],{},"I wouldn't block commits with it",". CI already gives you access to the same checks, with a controlled way to override them when you need to.",[30,264,265],{},"Enforcing blocking pre-commit hooks in private teams is rarely worth it:",[267,268,269,280,283],"ol",{},[47,270,271,272,275,276,279],{},"These rules can be easily disabled with ",[185,273,274],{},"HUSKY=0"," (or ",[185,277,278],{},"--no-verify",") without any trace or report.",[47,281,282],{},"If skipping them is somehow blocked (custom git wrappers, enforced hook policies, etc.), then glhf with hotfixes you need to release ASAP — just imagine explaining to a manager that you haven't released the fix due to an ESLint error. Also it makes temporary commits unavailable (I often use them).",[47,284,285],{},"Even disabling doesn't fully turn Husky rules off — e.g. a user can just delete the rules or Husky on their machine.",[30,287,288],{},"As a result, congrats: you made the workflow much worse by creating a half-secure pipeline.",[30,290,291],{},"Treating local Husky hooks as a real security boundary is a bit like storing a JWT secret in the browser. In interviews, if teams has a blocking Husky rules, I really consider this as a yellow-to-red flag.",[30,293,294],{},"Please, use Husky only if you know what you're doing.",[67,296,298],{"id":297},"dependency-versioning-taze-renovate","Dependency versioning = taze + Renovate",[30,300,301],{},"Managing dependencies is relatively easy, but it should be done repeatedly. If we could automate some of its parts even a little bit — we would get a big improvement long term.",[30,303,304,305,310],{},"You can check each package manually, or you could use ",[75,306,309],{"href":307,"rel":308},"https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Ftaze",[79],"taze",". Taze lets you check which packages have updates, categorize them by semver (major, minor, patch), and auto-apply them:",[179,312,316],{"className":313,"code":314,"language":315,"meta":12,"style":12},"language-bash shiki shiki-themes github-light github-dark","pnpm dlx taze -w\n","bash",[185,317,318],{"__ignoreMap":12},[188,319,320,324,327,330],{"class":190,"line":191},[188,321,323],{"class":322},"sScJk","pnpm",[188,325,326],{"class":208}," dlx",[188,328,329],{"class":208}," taze",[188,331,332],{"class":201}," -w\n",[30,334,335,336,341,342,81],{},"As an alternative you could use the ",[75,337,340],{"href":338,"rel":339},"https:\u002F\u002Fdocs.renovatebot.com\u002F",[79],"Renovate"," bot (or any similar one). It lets you create PRs automatically while managing your ",[185,343,344],{},"package.json",[30,346,347,348,350,351,356],{},"I honestly prefer ",[112,349,309],{}," as it works with ",[75,352,355],{"href":353,"rel":354},"https:\u002F\u002Fpnpm.io\u002Fcatalogs",[79],"pnpm catalogs"," and I like to test the build locally after each package update just to be sure. But both choices are solid for production in my opinion.",[67,358,360],{"id":359},"manage-monorepo-turborepo-pnpm","Manage monorepo = Turborepo + pnpm",[30,362,363],{},"Monorepos have a dark history — they were mostly a collection of workarounds to make all repos work together. Right now monorepos are much easier, but still harder to manage than a simple repo.",[30,365,366,367,127,372,376],{},"I've created and maintained many monorepos and I find ",[75,368,371],{"href":369,"rel":370},"https:\u002F\u002Fturbo.build\u002F",[79],"Turborepo",[75,373,323],{"href":374,"rel":375},"https:\u002F\u002Fpnpm.io\u002F",[79]," extremely useful.",[30,378,379,380,383,384,139,389,139,394,399],{},"I honestly think that ",[112,381,382],{},"pnpm should be used instead of npm by default"," in any repo — it's just faster and more space-efficient. Just some sources: ",[75,385,388],{"href":386,"rel":387},"https:\u002F\u002Fdev.to\u002Fpockit_tools\u002Fpnpm-vs-npm-vs-yarn-vs-bun-the-2026-package-manager-showdown-51dc",[79],"pnpm vs npm vs yarn vs Bun",[75,390,393],{"href":391,"rel":392},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=ZIKDJBrk56k",[79],"I Finally Changed Package Managers",[75,395,398],{"href":396,"rel":397},"https:\u002F\u002Fantfu.me\u002Fposts\u002Fcategorize-deps",[79],"Categorize Your Dependencies",". As for monorepos — pnpm is already established as a standard, where npm is trying to keep up (e.g. npm added workspaces only in 2020 and still doesn't have catalogs).",[30,401,402,405],{},[75,403,371],{"href":369,"rel":404},[79]," is a configurable build system which adds lots of features like build cache (so you wouldn't rebuild your workspace packages every time you rebuild the facade app) and a central command orchestrator (so you can run lint on all workspaces).",[67,407,409],{"id":408},"manage-package-versions-changesets","Manage package versions = Changesets",[30,411,412],{},"Managing versions for packages is much harder than you might initially think. You have to implement the desired change, test it, calculate which semver modification it belongs to, build and release the package, and then update the changelog. Repeat for the next version.",[30,414,415],{},"It becomes more complicated if we consider that we may want to batch changes before release, automate changelog updates, calculate the actual semver bump (if we had several changes with different semver levels), and — by the end — make this work collaborative.",[30,417,418,419,424],{},"I consider ",[75,420,423],{"href":421,"rel":422},"https:\u002F\u002Fgithub.com\u002Fchangesets\u002Fchangesets",[79],"Changesets"," to be the best tool for this case. Changesets lets you automate all these processes. You just need to fill a simple CLI form on each change. After that, the whole release pipeline can be automated as finely as you like.",[67,426,428],{"id":427},"generate-backend-endpoints-openapi-orval","Generate backend endpoints = OpenAPI + Orval",[30,430,431,432,437,438,443],{},"What if we want full type safety and endpoint synchronization between our backend and frontend? In SSR frameworks we can use tools like ",[75,433,436],{"href":434,"rel":435},"https:\u002F\u002Ftrpc.io\u002F",[79],"tRPC"," or Nuxt's auto-typed ",[75,439,442],{"href":440,"rel":441},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-fetch",[79],"useFetch"," composable. We can't apply the same methods though if we have a standard old-school SPA with a frontend team and a Java\u002FPHP\u002Fany-other-language backend team.",[30,445,446,447,452],{},"Tools like ",[75,448,451],{"href":449,"rel":450},"https:\u002F\u002Fgraphql.org\u002F",[79],"GraphQL"," exist, but what if you want just typed REST clients without adopting GraphQL? If you have such a case, you probably already have OpenAPI documentation from the backend to power Swagger. We can use codegen to take those same docs and generate Zod schemas, types, and even fetch services.",[30,454,455,456,461],{},"The best one in my opinion is ",[75,457,460],{"href":458,"rel":459},"https:\u002F\u002Forval.dev\u002F",[79],"Orval",". Orval is a codegen which takes OpenAPI docs and generates all of the above, plus nice docs and easy-to-use configuration.",[30,463,464],{},"Here's a concrete before \u002F after. Start with a tiny OpenAPI spec and an Orval config:",[179,466,470],{"className":467,"code":468,"language":469,"meta":12,"style":12},"language-yaml shiki shiki-themes github-light github-dark","# openapi.yaml\nopenapi: 3.0.3\ninfo:\n  title: Users API\n  version: 1.0.0\npaths:\n  \u002Fusers\u002F{id}:\n    get:\n      operationId: getUser\n      parameters:\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        '200':\n          description: OK\n          content:\n            application\u002Fjson:\n              schema:\n                $ref: '#\u002Fcomponents\u002Fschemas\u002FUser'\ncomponents:\n  schemas:\n    User:\n      type: object\n      required: [id, email]\n      properties:\n        id:\n          type: string\n        email:\n          type: string\n          format: email\n","yaml",[185,471,472,478,490,498,508,519,527,535,543,554,562,576,587,598,606,617,625,633,644,652,660,668,679,687,695,703,714,732,740,748,758,766,775],{"__ignoreMap":12},[188,473,474],{"class":190,"line":191},[188,475,477],{"class":476},"sJ8bj","# openapi.yaml\n",[188,479,480,484,487],{"class":190,"line":198},[188,481,483],{"class":482},"s9eBZ","openapi",[188,485,486],{"class":194},": ",[188,488,489],{"class":201},"3.0.3\n",[188,491,492,495],{"class":190,"line":220},[188,493,494],{"class":482},"info",[188,496,497],{"class":194},":\n",[188,499,500,503,505],{"class":190,"line":234},[188,501,502],{"class":482},"  title",[188,504,486],{"class":194},[188,506,507],{"class":208},"Users API\n",[188,509,511,514,516],{"class":190,"line":510},5,[188,512,513],{"class":482},"  version",[188,515,486],{"class":194},[188,517,518],{"class":201},"1.0.0\n",[188,520,522,525],{"class":190,"line":521},6,[188,523,524],{"class":482},"paths",[188,526,497],{"class":194},[188,528,530,533],{"class":190,"line":529},7,[188,531,532],{"class":482},"  \u002Fusers\u002F{id}",[188,534,497],{"class":194},[188,536,538,541],{"class":190,"line":537},8,[188,539,540],{"class":482},"    get",[188,542,497],{"class":194},[188,544,546,549,551],{"class":190,"line":545},9,[188,547,548],{"class":482},"      operationId",[188,550,486],{"class":194},[188,552,553],{"class":208},"getUser\n",[188,555,557,560],{"class":190,"line":556},10,[188,558,559],{"class":482},"      parameters",[188,561,497],{"class":194},[188,563,565,568,571,573],{"class":190,"line":564},11,[188,566,567],{"class":194},"        - ",[188,569,570],{"class":482},"name",[188,572,486],{"class":194},[188,574,575],{"class":208},"id\n",[188,577,579,582,584],{"class":190,"line":578},12,[188,580,581],{"class":482},"          in",[188,583,486],{"class":194},[188,585,586],{"class":208},"path\n",[188,588,590,593,595],{"class":190,"line":589},13,[188,591,592],{"class":482},"          required",[188,594,486],{"class":194},[188,596,597],{"class":201},"true\n",[188,599,601,604],{"class":190,"line":600},14,[188,602,603],{"class":482},"          schema",[188,605,497],{"class":194},[188,607,609,612,614],{"class":190,"line":608},15,[188,610,611],{"class":482},"            type",[188,613,486],{"class":194},[188,615,616],{"class":208},"string\n",[188,618,620,623],{"class":190,"line":619},16,[188,621,622],{"class":482},"      responses",[188,624,497],{"class":194},[188,626,628,631],{"class":190,"line":627},17,[188,629,630],{"class":208},"        '200'",[188,632,497],{"class":194},[188,634,636,639,641],{"class":190,"line":635},18,[188,637,638],{"class":482},"          description",[188,640,486],{"class":194},[188,642,643],{"class":208},"OK\n",[188,645,647,650],{"class":190,"line":646},19,[188,648,649],{"class":482},"          content",[188,651,497],{"class":194},[188,653,655,658],{"class":190,"line":654},20,[188,656,657],{"class":482},"            application\u002Fjson",[188,659,497],{"class":194},[188,661,663,666],{"class":190,"line":662},21,[188,664,665],{"class":482},"              schema",[188,667,497],{"class":194},[188,669,671,674,676],{"class":190,"line":670},22,[188,672,673],{"class":482},"                $ref",[188,675,486],{"class":194},[188,677,678],{"class":208},"'#\u002Fcomponents\u002Fschemas\u002FUser'\n",[188,680,682,685],{"class":190,"line":681},23,[188,683,684],{"class":482},"components",[188,686,497],{"class":194},[188,688,690,693],{"class":190,"line":689},24,[188,691,692],{"class":482},"  schemas",[188,694,497],{"class":194},[188,696,698,701],{"class":190,"line":697},25,[188,699,700],{"class":482},"    User",[188,702,497],{"class":194},[188,704,706,709,711],{"class":190,"line":705},26,[188,707,708],{"class":482},"      type",[188,710,486],{"class":194},[188,712,713],{"class":208},"object\n",[188,715,717,720,722,725,727,730],{"class":190,"line":716},27,[188,718,719],{"class":482},"      required",[188,721,205],{"class":194},[188,723,724],{"class":208},"id",[188,726,139],{"class":194},[188,728,729],{"class":208},"email",[188,731,231],{"class":194},[188,733,735,738],{"class":190,"line":734},28,[188,736,737],{"class":482},"      properties",[188,739,497],{"class":194},[188,741,743,746],{"class":190,"line":742},29,[188,744,745],{"class":482},"        id",[188,747,497],{"class":194},[188,749,751,754,756],{"class":190,"line":750},30,[188,752,753],{"class":482},"          type",[188,755,486],{"class":194},[188,757,616],{"class":208},[188,759,761,764],{"class":190,"line":760},31,[188,762,763],{"class":482},"        email",[188,765,497],{"class":194},[188,767,769,771,773],{"class":190,"line":768},32,[188,770,753],{"class":482},[188,772,486],{"class":194},[188,774,616],{"class":208},[188,776,778,781,783],{"class":190,"line":777},33,[188,779,780],{"class":482},"          format",[188,782,486],{"class":194},[188,784,785],{"class":208},"email\n",[179,787,791],{"className":788,"code":789,"language":790,"meta":12,"style":12},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F orval.config.ts\nimport { defineConfig } from 'orval'\n\nexport default defineConfig({\n  api: {\n    input: '.\u002Fopenapi.yaml',\n    output: {\n      target: '.\u002Fsrc\u002Fapi\u002Fendpoints.ts',\n      client: 'fetch',\n      schemas: '.\u002Fsrc\u002Fapi\u002Fmodels',\n    },\n  },\n})\n","ts",[185,792,793,798,813,819,833,838,849,854,864,874,884,889,894],{"__ignoreMap":12},[188,794,795],{"class":190,"line":191},[188,796,797],{"class":476},"\u002F\u002F orval.config.ts\n",[188,799,800,804,807,810],{"class":190,"line":198},[188,801,803],{"class":802},"szBVR","import",[188,805,806],{"class":194}," { defineConfig } ",[188,808,809],{"class":802},"from",[188,811,812],{"class":208}," 'orval'\n",[188,814,815],{"class":190,"line":220},[188,816,818],{"emptyLinePlaceholder":817},true,"\n",[188,820,821,824,827,830],{"class":190,"line":234},[188,822,823],{"class":802},"export",[188,825,826],{"class":802}," default",[188,828,829],{"class":322}," defineConfig",[188,831,832],{"class":194},"({\n",[188,834,835],{"class":190,"line":510},[188,836,837],{"class":194},"  api: {\n",[188,839,840,843,846],{"class":190,"line":521},[188,841,842],{"class":194},"    input: ",[188,844,845],{"class":208},"'.\u002Fopenapi.yaml'",[188,847,848],{"class":194},",\n",[188,850,851],{"class":190,"line":529},[188,852,853],{"class":194},"    output: {\n",[188,855,856,859,862],{"class":190,"line":537},[188,857,858],{"class":194},"      target: ",[188,860,861],{"class":208},"'.\u002Fsrc\u002Fapi\u002Fendpoints.ts'",[188,863,848],{"class":194},[188,865,866,869,872],{"class":190,"line":545},[188,867,868],{"class":194},"      client: ",[188,870,871],{"class":208},"'fetch'",[188,873,848],{"class":194},[188,875,876,879,882],{"class":190,"line":556},[188,877,878],{"class":194},"      schemas: ",[188,880,881],{"class":208},"'.\u002Fsrc\u002Fapi\u002Fmodels'",[188,883,848],{"class":194},[188,885,886],{"class":190,"line":564},[188,887,888],{"class":194},"    },\n",[188,890,891],{"class":190,"line":578},[188,892,893],{"class":194},"  },\n",[188,895,896],{"class":190,"line":589},[188,897,898],{"class":194},"})\n",[30,900,901,902,905],{},"After ",[185,903,904],{},"orval",", you get typed models and a ready-to-use fetch client — no hand-written DTO mapping:",[179,907,909],{"className":788,"code":908,"language":790,"meta":12,"style":12},"\u002F\u002F src\u002Fapi\u002Fmodels\u002Fuser.ts (generated)\nexport interface User {\n  id: string\n  email: string\n}\n\n\u002F\u002F src\u002Fapi\u002Fendpoints.ts (generated, simplified)\nexport const getUser = async (id: string, options?: RequestInit): Promise\u003CUser> => {\n  const res = await fetch(`\u002Fusers\u002F${id}`, { ...options, method: 'GET' })\n  return res.json()\n}\n",[185,910,911,916,929,941,950,954,958,963,1022,1064,1077],{"__ignoreMap":12},[188,912,913],{"class":190,"line":191},[188,914,915],{"class":476},"\u002F\u002F src\u002Fapi\u002Fmodels\u002Fuser.ts (generated)\n",[188,917,918,920,923,926],{"class":190,"line":198},[188,919,823],{"class":802},[188,921,922],{"class":802}," interface",[188,924,925],{"class":322}," User",[188,927,928],{"class":194}," {\n",[188,930,931,935,938],{"class":190,"line":220},[188,932,934],{"class":933},"s4XuR","  id",[188,936,937],{"class":802},":",[188,939,940],{"class":201}," string\n",[188,942,943,946,948],{"class":190,"line":234},[188,944,945],{"class":933},"  email",[188,947,937],{"class":802},[188,949,940],{"class":201},[188,951,952],{"class":190,"line":510},[188,953,237],{"class":194},[188,955,956],{"class":190,"line":521},[188,957,818],{"emptyLinePlaceholder":817},[188,959,960],{"class":190,"line":529},[188,961,962],{"class":476},"\u002F\u002F src\u002Fapi\u002Fendpoints.ts (generated, simplified)\n",[188,964,965,967,970,973,976,979,982,984,986,989,991,994,997,1000,1003,1005,1008,1011,1014,1017,1020],{"class":190,"line":537},[188,966,823],{"class":802},[188,968,969],{"class":802}," const",[188,971,972],{"class":322}," getUser",[188,974,975],{"class":802}," =",[188,977,978],{"class":802}," async",[188,980,981],{"class":194}," (",[188,983,724],{"class":933},[188,985,937],{"class":802},[188,987,988],{"class":201}," string",[188,990,139],{"class":194},[188,992,993],{"class":933},"options",[188,995,996],{"class":802},"?:",[188,998,999],{"class":322}," RequestInit",[188,1001,1002],{"class":194},")",[188,1004,937],{"class":802},[188,1006,1007],{"class":322}," Promise",[188,1009,1010],{"class":194},"\u003C",[188,1012,1013],{"class":322},"User",[188,1015,1016],{"class":194},"> ",[188,1018,1019],{"class":802},"=>",[188,1021,928],{"class":194},[188,1023,1024,1027,1030,1032,1035,1038,1041,1044,1046,1049,1052,1055,1058,1061],{"class":190,"line":545},[188,1025,1026],{"class":802},"  const",[188,1028,1029],{"class":201}," res",[188,1031,975],{"class":802},[188,1033,1034],{"class":802}," await",[188,1036,1037],{"class":322}," fetch",[188,1039,1040],{"class":194},"(",[188,1042,1043],{"class":208},"`\u002Fusers\u002F${",[188,1045,724],{"class":194},[188,1047,1048],{"class":208},"}`",[188,1050,1051],{"class":194},", { ",[188,1053,1054],{"class":802},"...",[188,1056,1057],{"class":194},"options, method: ",[188,1059,1060],{"class":208},"'GET'",[188,1062,1063],{"class":194}," })\n",[188,1065,1066,1069,1072,1074],{"class":190,"line":556},[188,1067,1068],{"class":802},"  return",[188,1070,1071],{"class":194}," res.",[188,1073,183],{"class":322},[188,1075,1076],{"class":194},"()\n",[188,1078,1079],{"class":190,"line":564},[188,1080,237],{"class":194},[30,1082,1083],{},"The one problem I want to mention is that you need to manage collaborative work, which could change and break the types on the frontend (e.g. you are writing a hotfix and some feature is deployed on dev by the backend team, which results in your types being broken). For this case, one solution is to use an npm package with OpenAPI schemas, which would be updated and versioned by both teams.",[67,1085,1087],{"id":1086},"generate-figma-tokens-tokens-studio-style-dictionary","Generate Figma tokens = Tokens Studio + Style Dictionary",[30,1089,1090],{},"This is both the most useful and the hardest automation to set up, as it would require changing the way your design team works.",[30,1092,1093],{},"The major issue when you have a designed UI is keeping it in sync with changes in Figma. I've heard lots of scary stories about teams spending even 1\u002F5 of their sprints just syncing with Figma. For some reason there's a still poor versioning mechanism in Figma, and many local custom workarounds are created.",[30,1095,1096,1097,1102,1103,1108],{},"We can use ",[75,1098,1101],{"href":1099,"rel":1100},"https:\u002F\u002Ftokens.studio\u002F",[79],"Tokens Studio"," to resolve this issue. Tokens Studio is basically a Figma plugin which manages design tokens. When designers use tokens through that plugin they can export them as a schema (like OpenAPI for tokens), and then we can use ",[75,1104,1107],{"href":1105,"rel":1106},"https:\u002F\u002Fstyledictionary.com\u002F",[79],"Style Dictionary"," to render this schema into our specific CSS setup — Tailwind, CSS variables, Sass, and so on.",[30,1110,1111],{},"Concrete before \u002F after. Designers export a Tokens Studio schema, and you point Style Dictionary at it:",[179,1113,1115],{"className":181,"code":1114,"language":183,"meta":12,"style":12},"\u002F\u002F tokens\u002Fcolors.json (exported from Tokens Studio)\n{\n  \"color\": {\n    \"brand\": {\n      \"primary\": { \"value\": \"#2563eb\", \"type\": \"color\" },\n      \"secondary\": { \"value\": \"#7c3aed\", \"type\": \"color\" }\n    },\n    \"surface\": {\n      \"bg\": { \"value\": \"#ffffff\", \"type\": \"color\" },\n      \"fg\": { \"value\": \"#0f172a\", \"type\": \"color\" }\n    }\n  },\n  \"spacing\": {\n    \"sm\": { \"value\": \"8\", \"type\": \"spacing\" },\n    \"md\": { \"value\": \"16\", \"type\": \"spacing\" },\n    \"lg\": { \"value\": \"24\", \"type\": \"spacing\" }\n  }\n}\n",[185,1116,1117,1122,1126,1134,1141,1170,1195,1199,1206,1230,1254,1259,1263,1270,1295,1319,1343,1348],{"__ignoreMap":12},[188,1118,1119],{"class":190,"line":191},[188,1120,1121],{"class":476},"\u002F\u002F tokens\u002Fcolors.json (exported from Tokens Studio)\n",[188,1123,1124],{"class":190,"line":198},[188,1125,195],{"class":194},[188,1127,1128,1131],{"class":190,"line":220},[188,1129,1130],{"class":201},"  \"color\"",[188,1132,1133],{"class":194},": {\n",[188,1135,1136,1139],{"class":190,"line":234},[188,1137,1138],{"class":201},"    \"brand\"",[188,1140,1133],{"class":194},[188,1142,1143,1146,1149,1152,1154,1157,1159,1162,1164,1167],{"class":190,"line":510},[188,1144,1145],{"class":201},"      \"primary\"",[188,1147,1148],{"class":194},": { ",[188,1150,1151],{"class":201},"\"value\"",[188,1153,486],{"class":194},[188,1155,1156],{"class":208},"\"#2563eb\"",[188,1158,139],{"class":194},[188,1160,1161],{"class":201},"\"type\"",[188,1163,486],{"class":194},[188,1165,1166],{"class":208},"\"color\"",[188,1168,1169],{"class":194}," },\n",[188,1171,1172,1175,1177,1179,1181,1184,1186,1188,1190,1192],{"class":190,"line":521},[188,1173,1174],{"class":201},"      \"secondary\"",[188,1176,1148],{"class":194},[188,1178,1151],{"class":201},[188,1180,486],{"class":194},[188,1182,1183],{"class":208},"\"#7c3aed\"",[188,1185,139],{"class":194},[188,1187,1161],{"class":201},[188,1189,486],{"class":194},[188,1191,1166],{"class":208},[188,1193,1194],{"class":194}," }\n",[188,1196,1197],{"class":190,"line":529},[188,1198,888],{"class":194},[188,1200,1201,1204],{"class":190,"line":537},[188,1202,1203],{"class":201},"    \"surface\"",[188,1205,1133],{"class":194},[188,1207,1208,1211,1213,1215,1217,1220,1222,1224,1226,1228],{"class":190,"line":545},[188,1209,1210],{"class":201},"      \"bg\"",[188,1212,1148],{"class":194},[188,1214,1151],{"class":201},[188,1216,486],{"class":194},[188,1218,1219],{"class":208},"\"#ffffff\"",[188,1221,139],{"class":194},[188,1223,1161],{"class":201},[188,1225,486],{"class":194},[188,1227,1166],{"class":208},[188,1229,1169],{"class":194},[188,1231,1232,1235,1237,1239,1241,1244,1246,1248,1250,1252],{"class":190,"line":556},[188,1233,1234],{"class":201},"      \"fg\"",[188,1236,1148],{"class":194},[188,1238,1151],{"class":201},[188,1240,486],{"class":194},[188,1242,1243],{"class":208},"\"#0f172a\"",[188,1245,139],{"class":194},[188,1247,1161],{"class":201},[188,1249,486],{"class":194},[188,1251,1166],{"class":208},[188,1253,1194],{"class":194},[188,1255,1256],{"class":190,"line":564},[188,1257,1258],{"class":194},"    }\n",[188,1260,1261],{"class":190,"line":578},[188,1262,893],{"class":194},[188,1264,1265,1268],{"class":190,"line":589},[188,1266,1267],{"class":201},"  \"spacing\"",[188,1269,1133],{"class":194},[188,1271,1272,1275,1277,1279,1281,1284,1286,1288,1290,1293],{"class":190,"line":600},[188,1273,1274],{"class":201},"    \"sm\"",[188,1276,1148],{"class":194},[188,1278,1151],{"class":201},[188,1280,486],{"class":194},[188,1282,1283],{"class":208},"\"8\"",[188,1285,139],{"class":194},[188,1287,1161],{"class":201},[188,1289,486],{"class":194},[188,1291,1292],{"class":208},"\"spacing\"",[188,1294,1169],{"class":194},[188,1296,1297,1300,1302,1304,1306,1309,1311,1313,1315,1317],{"class":190,"line":608},[188,1298,1299],{"class":201},"    \"md\"",[188,1301,1148],{"class":194},[188,1303,1151],{"class":201},[188,1305,486],{"class":194},[188,1307,1308],{"class":208},"\"16\"",[188,1310,139],{"class":194},[188,1312,1161],{"class":201},[188,1314,486],{"class":194},[188,1316,1292],{"class":208},[188,1318,1169],{"class":194},[188,1320,1321,1324,1326,1328,1330,1333,1335,1337,1339,1341],{"class":190,"line":619},[188,1322,1323],{"class":201},"    \"lg\"",[188,1325,1148],{"class":194},[188,1327,1151],{"class":201},[188,1329,486],{"class":194},[188,1331,1332],{"class":208},"\"24\"",[188,1334,139],{"class":194},[188,1336,1161],{"class":201},[188,1338,486],{"class":194},[188,1340,1292],{"class":208},[188,1342,1194],{"class":194},[188,1344,1345],{"class":190,"line":627},[188,1346,1347],{"class":194},"  }\n",[188,1349,1350],{"class":190,"line":635},[188,1351,237],{"class":194},[179,1353,1357],{"className":1354,"code":1355,"language":1356,"meta":12,"style":12},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F style-dictionary.config.js\nexport default {\n  source: ['tokens\u002F**\u002F*.json'],\n  platforms: {\n    css: {\n      transformGroup: 'css',\n      buildPath: 'build\u002F',\n      files: [\n        {\n          destination: 'theme.css',\n          format: 'css\u002Fvariables',\n          options: {\n            selector: '@theme',\n          },\n        },\n      ],\n    },\n  },\n}\n","js",[185,1358,1359,1364,1372,1382,1387,1392,1402,1412,1417,1422,1432,1442,1447,1457,1462,1467,1472,1476,1480],{"__ignoreMap":12},[188,1360,1361],{"class":190,"line":191},[188,1362,1363],{"class":476},"\u002F\u002F style-dictionary.config.js\n",[188,1365,1366,1368,1370],{"class":190,"line":198},[188,1367,823],{"class":802},[188,1369,826],{"class":802},[188,1371,928],{"class":194},[188,1373,1374,1377,1380],{"class":190,"line":220},[188,1375,1376],{"class":194},"  source: [",[188,1378,1379],{"class":208},"'tokens\u002F**\u002F*.json'",[188,1381,217],{"class":194},[188,1383,1384],{"class":190,"line":234},[188,1385,1386],{"class":194},"  platforms: {\n",[188,1388,1389],{"class":190,"line":510},[188,1390,1391],{"class":194},"    css: {\n",[188,1393,1394,1397,1400],{"class":190,"line":521},[188,1395,1396],{"class":194},"      transformGroup: ",[188,1398,1399],{"class":208},"'css'",[188,1401,848],{"class":194},[188,1403,1404,1407,1410],{"class":190,"line":529},[188,1405,1406],{"class":194},"      buildPath: ",[188,1408,1409],{"class":208},"'build\u002F'",[188,1411,848],{"class":194},[188,1413,1414],{"class":190,"line":537},[188,1415,1416],{"class":194},"      files: [\n",[188,1418,1419],{"class":190,"line":545},[188,1420,1421],{"class":194},"        {\n",[188,1423,1424,1427,1430],{"class":190,"line":556},[188,1425,1426],{"class":194},"          destination: ",[188,1428,1429],{"class":208},"'theme.css'",[188,1431,848],{"class":194},[188,1433,1434,1437,1440],{"class":190,"line":564},[188,1435,1436],{"class":194},"          format: ",[188,1438,1439],{"class":208},"'css\u002Fvariables'",[188,1441,848],{"class":194},[188,1443,1444],{"class":190,"line":578},[188,1445,1446],{"class":194},"          options: {\n",[188,1448,1449,1452,1455],{"class":190,"line":589},[188,1450,1451],{"class":194},"            selector: ",[188,1453,1454],{"class":208},"'@theme'",[188,1456,848],{"class":194},[188,1458,1459],{"class":190,"line":600},[188,1460,1461],{"class":194},"          },\n",[188,1463,1464],{"class":190,"line":608},[188,1465,1466],{"class":194},"        },\n",[188,1468,1469],{"class":190,"line":619},[188,1470,1471],{"class":194},"      ],\n",[188,1473,1474],{"class":190,"line":627},[188,1475,888],{"class":194},[188,1477,1478],{"class":190,"line":635},[188,1479,893],{"class":194},[188,1481,1482],{"class":190,"line":646},[188,1483,237],{"class":194},[30,1485,1486,1487,1490],{},"With ",[185,1488,1489],{},"selector: '@theme'",", Style Dictionary generates a Tailwind v4 theme file directly:",[179,1492,1496],{"className":1493,"code":1494,"language":1495,"meta":12,"style":12},"language-css shiki shiki-themes github-light github-dark","\u002F**\n * Do not edit directly, this file was auto-generated.\n *\u002F\n\n@theme {\n  --color-brand-primary: #2563eb;\n  --color-brand-secondary: #7c3aed;\n  --color-surface-bg: #ffffff;\n  --color-surface-fg: #0f172a;\n  --spacing-sm: 8;\n  --spacing-md: 16;\n  --spacing-lg: 24;\n}\n","css",[185,1497,1498,1503,1508,1513,1517,1524,1536,1546,1554,1564,1569,1574,1579],{"__ignoreMap":12},[188,1499,1500],{"class":190,"line":191},[188,1501,1502],{"class":476},"\u002F**\n",[188,1504,1505],{"class":190,"line":198},[188,1506,1507],{"class":476}," * Do not edit directly, this file was auto-generated.\n",[188,1509,1510],{"class":190,"line":220},[188,1511,1512],{"class":476}," *\u002F\n",[188,1514,1515],{"class":190,"line":234},[188,1516,818],{"emptyLinePlaceholder":817},[188,1518,1519,1522],{"class":190,"line":510},[188,1520,1521],{"class":802},"@theme",[188,1523,928],{"class":194},[188,1525,1526,1529,1533],{"class":190,"line":521},[188,1527,1528],{"class":194},"  --color-brand-primary: ",[188,1530,1532],{"class":1531},"s7hpK","#2563eb",[188,1534,1535],{"class":194},";\n",[188,1537,1538,1541,1544],{"class":190,"line":529},[188,1539,1540],{"class":194},"  --color-brand-secondary: ",[188,1542,1543],{"class":1531},"#7c3aed",[188,1545,1535],{"class":194},[188,1547,1548,1551],{"class":190,"line":537},[188,1549,1550],{"class":194},"  --color-surface-bg: ",[188,1552,1553],{"class":1531},"#ffffff;\n",[188,1555,1556,1559,1562],{"class":190,"line":545},[188,1557,1558],{"class":194},"  --color-surface-fg: ",[188,1560,1561],{"class":1531},"#0f172a",[188,1563,1535],{"class":194},[188,1565,1566],{"class":190,"line":556},[188,1567,1568],{"class":194},"  --spacing-sm: 8;\n",[188,1570,1571],{"class":190,"line":564},[188,1572,1573],{"class":194},"  --spacing-md: 16;\n",[188,1575,1576],{"class":190,"line":578},[188,1577,1578],{"class":194},"  --spacing-lg: 24;\n",[188,1580,1581],{"class":190,"line":589},[188,1582,237],{"class":194},[179,1584,1586],{"className":1493,"code":1585,"language":1495,"meta":12,"style":12},"\u002F* app.css *\u002F\n@import \"tailwindcss\";\n@import \".\u002Fbuild\u002Ftheme.css\";\n",[185,1587,1588,1593,1603],{"__ignoreMap":12},[188,1589,1590],{"class":190,"line":191},[188,1591,1592],{"class":476},"\u002F* app.css *\u002F\n",[188,1594,1595,1598,1601],{"class":190,"line":198},[188,1596,1597],{"class":802},"@import",[188,1599,1600],{"class":208}," \"tailwindcss\"",[188,1602,1535],{"class":194},[188,1604,1605,1607,1610],{"class":190,"line":220},[188,1606,1597],{"class":802},[188,1608,1609],{"class":208}," \".\u002Fbuild\u002Ftheme.css\"",[188,1611,1535],{"class":194},[30,1613,1614],{},"It has the same issue with versioning the tokens, but Tokens Studio supports integration with Git so designers can easily version token changes. Then we can create an npm package where this generation would be applied and pin our frontend to that version.",[25,1616,1618],{"id":1617},"conclusion","Conclusion",[30,1620,1621],{},"I've tried to collect my favorite high-value automations, the ones that I actually kept using after trying. I hope you found something interesting and new for yourself!",[1623,1624,1625],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":12,"searchDepth":198,"depth":198,"links":1627},[1628,1629,1630,1640],{"id":27,"depth":198,"text":28},{"id":38,"depth":198,"text":39},{"id":61,"depth":198,"text":62,"children":1631},[1632,1633,1634,1635,1636,1637,1638,1639],{"id":69,"depth":220,"text":70},{"id":148,"depth":220,"text":149},{"id":168,"depth":220,"text":169},{"id":297,"depth":220,"text":298},{"id":359,"depth":220,"text":360},{"id":408,"depth":220,"text":409},{"id":427,"depth":220,"text":428},{"id":1086,"depth":220,"text":1087},{"id":1617,"depth":198,"text":1618},"2026-07-21 00:00:00 UTC","Some useful automations I discovered to drastically improve the developer experience","10 min","md",{},"\u002Fposts\u002Fessential-frontend-automations",null,{"title":5,"description":1642},{"loc":1646},"posts\u002Fessential-frontend-automations",[1652],"dev","e0iDv3EXENc_stCWeI5QtJzDZ817zbHJJto7OES4pU0",1790706793656]