[{"data":1,"prerenderedAt":1751},["ShallowReactive",2],{"navigation":3,"article-/articles/2026-04-18-validation-across-the-stack":146,"article-surround-/articles/2026-04-18-validation-across-the-stack":1451,"tags-list":1456},[4],{"title":5,"path":6,"stem":7,"children":8,"page":145},"Articles","/articles","articles",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65,69,73,77,81,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141],{"title":10,"path":11,"stem":12},"Building a Claude Code Plugin with Hooks","/articles/2026-01-04-building-claude-code-plugin-with-hooks","articles/2026-01-04-building-claude-code-plugin-with-hooks",{"title":14,"path":15,"stem":16},"Where should I validate? Everywhere, and for different reasons","/articles/2026-04-18-validation-across-the-stack","articles/2026-04-18-validation-across-the-stack",{"title":18,"path":19,"stem":20},"Error handling across the stack","/articles/2026-04-23-error-handling-across-the-stack","articles/2026-04-23-error-handling-across-the-stack",{"title":22,"path":23,"stem":24},"3 Ways to Run NestJS Cron Jobs When Running Multiple Instances","/articles/3-ways-to-run-nestjs-cron-jobs-when-running-multiple-instances","articles/3-ways-to-run-nestjs-cron-jobs-when-running-multiple-instances",{"title":26,"path":27,"stem":28},"Building a Custom Content Slider (Carousel) in Angular","/articles/building-a-custom-content-slider-carousel-in-angular","articles/building-a-custom-content-slider-carousel-in-angular",{"title":30,"path":31,"stem":32},"Building Jobven: Designing a Developer-Friendly API","/articles/building-jobven-designing-a-developer-friendly-api","articles/building-jobven-designing-a-developer-friendly-api",{"title":34,"path":35,"stem":36},"Building MapleStack: AWS S3 for Data Storage","/articles/building-maplestack-aws-s3-for-data-storage","articles/building-maplestack-aws-s3-for-data-storage",{"title":38,"path":39,"stem":40},"Building MapleStack: Enhancing Email Capabilities with Mailgun","/articles/building-maplestack-enhancing-email-capabilities-with-mailgun","articles/building-maplestack-enhancing-email-capabilities-with-mailgun",{"title":42,"path":43,"stem":44},"Building MapleStack: NestJS for Server-Side Operations","/articles/building-maplestack-nestjs-for-server-side-operations","articles/building-maplestack-nestjs-for-server-side-operations",{"title":46,"path":47,"stem":48},"Building MapleStack: PostgreSQL for Data Storage","/articles/building-maplestack-postgresql-for-data-storage","articles/building-maplestack-postgresql-for-data-storage",{"title":50,"path":51,"stem":52},"Building MapleStack: React for an interactive user interface","/articles/building-maplestack-react-for-an-interactive-user-interface","articles/building-maplestack-react-for-an-interactive-user-interface",{"title":54,"path":55,"stem":56},"Building MapleStack: Securing Connections with Let's Encrypt","/articles/building-maplestack-securing-connections-with-lets-encrypt","articles/building-maplestack-securing-connections-with-lets-encrypt",{"title":58,"path":59,"stem":60},"Building MapleStack: Simplifying User Authorization with AWS Cognito","/articles/building-maplestack-simplifying-user-authorization-with-aws-cognito","articles/building-maplestack-simplifying-user-authorization-with-aws-cognito",{"title":62,"path":63,"stem":64},"Building MapleStack: Tailwind CSS for Streamlined Styling","/articles/building-maplestack-tailwind-css-for-streamlined-styling","articles/building-maplestack-tailwind-css-for-streamlined-styling",{"title":66,"path":67,"stem":68},"Debugging Multiple NestJS Applications in VSCode","/articles/debugging-multiple-nestjs-applications-in-vscode","articles/debugging-multiple-nestjs-applications-in-vscode",{"title":70,"path":71,"stem":72},"Enums as arrays in PostgreSQL - Updated 2024","/articles/enums-as-arrays-in-postgresql","articles/enums-as-arrays-in-postgresql",{"title":74,"path":75,"stem":76},"FindChildcare.ca","/articles/findchildcare-ca","articles/findchildcare-ca",{"title":78,"path":79,"stem":80},"GraphQL Server - Apollo, KoaJS and Typescript implementation","/articles/graphql-server-apollo-koajs-and-typescript-implementation","articles/graphql-server-apollo-koajs-and-typescript-implementation",{"title":82,"path":83,"stem":84},"How I built MapleStack's waitlist with AI: ChatGPT vs Bard","/articles/how-i-built-maplestacks-waitlist-with-ai-chatgpt-vs-bard","articles/how-i-built-maplestacks-waitlist-with-ai-chatgpt-vs-bard",{"title":86,"path":87,"stem":88},"How to Integrate Google reCAPTCHA v3 with NestJS in 3 Easy Steps","/articles/how-to-integrate-google-recaptcha-v3-with-nestjs-in-3-easy-steps","articles/how-to-integrate-google-recaptcha-v3-with-nestjs-in-3-easy-steps",{"title":90,"path":91,"stem":92},"How to resolve a blacklisted domain","/articles/how-to-resolve-a-blacklisted-domain","articles/how-to-resolve-a-blacklisted-domain",{"title":94,"path":95,"stem":96},"Implementing WordPress-like Tags in Nuxt Content","/articles/implementing-wordpress-like-tags-in-nuxt","articles/implementing-wordpress-like-tags-in-nuxt",{"title":98,"path":99,"stem":100},"Jobven","/articles/jobven","articles/jobven",{"title":102,"path":103,"stem":104},"KidzLog","/articles/kidzlog","articles/kidzlog",{"title":106,"path":107,"stem":108},"MapleStack","/articles/maplestack","articles/maplestack",{"title":110,"path":111,"stem":112},"Migrating from WordPress to Nuxt","/articles/migrating-from-wordpress-to-nuxt","articles/migrating-from-wordpress-to-nuxt",{"title":114,"path":115,"stem":116},"My Initial Server Setup Checklist","/articles/my-initial-server-setup-checklist","articles/my-initial-server-setup-checklist",{"title":118,"path":119,"stem":120},"Pre-authorized transaction on the Stellar network","/articles/pre-authorized-transaction-on-the-stellar-network","articles/pre-authorized-transaction-on-the-stellar-network",{"title":122,"path":123,"stem":124},"Saza: Open Source mobile and desktop Stellar wallet release.","/articles/saza-open-source-mobile-and-desktop-stellar-wallet-release","articles/saza-open-source-mobile-and-desktop-stellar-wallet-release",{"title":126,"path":127,"stem":128},"SpellCheckMySite.com","/articles/spellcheckmysite-com","articles/spellcheckmysite-com",{"title":130,"path":131,"stem":132},"Using Check Constraints in PostgreSQL for Value Validation","/articles/using-check-constraints-in-postgresql-for-value-validation","articles/using-check-constraints-in-postgresql-for-value-validation",{"title":134,"path":135,"stem":136},"Using DataLoader in GraphQL","/articles/using-dataloader-in-graphql","articles/using-dataloader-in-graphql",{"title":138,"path":139,"stem":140},"Using Typescript in NodeJS development","/articles/using-typescript-in-nodejs-development","articles/using-typescript-in-nodejs-development",{"title":142,"path":143,"stem":144},"Welcome!","/articles/welcome","articles/welcome",false,{"id":147,"title":14,"author":148,"body":152,"categories":1437,"date":1438,"description":1439,"extension":1440,"head":1441,"image":1442,"meta":1443,"minRead":427,"navigation":344,"ogImage":1441,"path":15,"robots":1441,"schemaOrg":1441,"seo":1444,"sitemap":1445,"stem":16,"tags":1446,"__hash__":1450},"articles/articles/2026-04-18-validation-across-the-stack.md",{"name":149,"avatar":150},"Peter Oliha",{"src":151,"alt":149},"/assets/images/authors/peter-oliha.jpg",{"type":153,"value":154,"toc":1421},"minimark",[155,159,162,165,168,173,176,179,182,186,191,194,209,431,446,453,457,460,472,752,758,762,765,768,788,1199,1202,1246,1249,1252,1255,1259,1262,1269,1272,1275,1316,1319,1323,1326,1329,1333,1336,1339,1343,1346,1349,1353,1371,1374,1378,1381,1391,1398,1402,1405,1408,1411,1414,1417],[156,157,158],"p",{},"There are two things every software engineer building products needs to take seriously. Yes there are others, but I want these two at the top of the list.",[156,160,161],{},"Validation and error handling.",[156,163,164],{},"This article is about the first one. I will write a follow up on error handling soon, because the two play well together and a lot of the patterns pair up nicely.",[156,166,167],{},"A quick note on the code. I spend most of my time in the NestJS ecosystem, so most of the samples below use class-validator decorators and a few custom constraints I have built up over the years. The patterns translate to whatever you are using (Zod, Yup, Vuelidate, vee-validate) and the point is not the syntax, it is the thinking.",[169,170,172],"h2",{"id":171},"validation-is-boring-until-it-isnt","Validation is boring until it isn't",[156,174,175],{},"Every \"weird bug in prod\" story I have ever heard has a missing validator somewhere in the middle of it. A form that accepts an empty string where it should not. A scraper that silently ingests HTML when it expected JSON. A trial that should have ended last Tuesday.",[156,177,178],{},"When you build products, they are rarely used the way you imagined. Validation is how you keep the gap between what you designed for and what actually shows up from getting out of hand.",[156,180,181],{},"I think of validation as falling into four flavours. They look similar on the surface but serve different purposes, and mixing them up is where I see people get stuck.",[169,183,185],{"id":184},"the-four-flavours","The four flavours",[187,188,190],"h3",{"id":189},"_1-user-input-validation","1. User input validation",[156,192,193],{},"This is what most people think of when they hear validation. Forms. API endpoints. Request bodies. You want the data your users send in to match the shape your app expects.",[156,195,196,197,201,202,208],{},"Here is a small piece of the ",[198,199,200],"code",{},"CreateParentDto"," from ",[203,204,102],"a",{"href":205,"rel":206},"https://kidzlog.com",[207],"nofollow",". It is the DTO that runs when a new parent is added to a childcare centre.",[210,211,216],"pre",{"className":212,"code":213,"language":214,"meta":215,"style":215},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export class CreateParentDto {\n  @IsOptional()\n  @ValidateIf(o => o.email !== undefined && o.email !== null && o.email !== '')\n  @IsEmail()\n  email?: string\n\n  @IsNotEmpty()\n  @IsUUID('4', { each: true, message: 'Invalid child id' })\n  children: string[]\n}\n","ts","",[198,217,218,239,253,316,326,339,346,356,411,425],{"__ignoreMap":215},[219,220,223,227,231,235],"span",{"class":221,"line":222},"line",1,[219,224,226],{"class":225},"s7zQu","export",[219,228,230],{"class":229},"spNyl"," class",[219,232,234],{"class":233},"sBMFI"," CreateParentDto",[219,236,238],{"class":237},"sMK4o"," {\n",[219,240,242,245,249],{"class":221,"line":241},2,[219,243,244],{"class":237},"  @",[219,246,248],{"class":247},"s2Zo4","IsOptional",[219,250,252],{"class":251},"sTEyZ","()\n",[219,254,256,258,261,264,268,271,274,277,280,283,286,289,291,293,295,297,300,302,304,306,308,310,313],{"class":221,"line":255},3,[219,257,244],{"class":237},[219,259,260],{"class":247},"ValidateIf",[219,262,263],{"class":251},"(",[219,265,267],{"class":266},"sHdIc","o",[219,269,270],{"class":229}," =>",[219,272,273],{"class":251}," o",[219,275,276],{"class":237},".",[219,278,279],{"class":251},"email ",[219,281,282],{"class":237},"!==",[219,284,285],{"class":237}," undefined",[219,287,288],{"class":237}," &&",[219,290,273],{"class":251},[219,292,276],{"class":237},[219,294,279],{"class":251},[219,296,282],{"class":237},[219,298,299],{"class":237}," null",[219,301,288],{"class":237},[219,303,273],{"class":251},[219,305,276],{"class":237},[219,307,279],{"class":251},[219,309,282],{"class":237},[219,311,312],{"class":237}," ''",[219,314,315],{"class":251},")\n",[219,317,319,321,324],{"class":221,"line":318},4,[219,320,244],{"class":237},[219,322,323],{"class":247},"IsEmail",[219,325,252],{"class":251},[219,327,329,333,336],{"class":221,"line":328},5,[219,330,332],{"class":331},"swJcz","  email",[219,334,335],{"class":237},"?:",[219,337,338],{"class":233}," string\n",[219,340,342],{"class":221,"line":341},6,[219,343,345],{"emptyLinePlaceholder":344},true,"\n",[219,347,349,351,354],{"class":221,"line":348},7,[219,350,244],{"class":237},[219,352,353],{"class":247},"IsNotEmpty",[219,355,252],{"class":251},[219,357,359,361,364,366,369,373,375,378,381,384,387,391,393,396,398,401,404,406,409],{"class":221,"line":358},8,[219,360,244],{"class":237},[219,362,363],{"class":247},"IsUUID",[219,365,263],{"class":251},[219,367,368],{"class":237},"'",[219,370,372],{"class":371},"sfazB","4",[219,374,368],{"class":237},[219,376,377],{"class":237},",",[219,379,380],{"class":237}," {",[219,382,383],{"class":331}," each",[219,385,386],{"class":237},":",[219,388,390],{"class":389},"sfNiH"," true",[219,392,377],{"class":237},[219,394,395],{"class":331}," message",[219,397,386],{"class":237},[219,399,400],{"class":237}," '",[219,402,403],{"class":371},"Invalid child id",[219,405,368],{"class":237},[219,407,408],{"class":237}," }",[219,410,315],{"class":251},[219,412,414,417,419,422],{"class":221,"line":413},9,[219,415,416],{"class":331},"  children",[219,418,386],{"class":237},[219,420,421],{"class":233}," string",[219,423,424],{"class":251},"[]\n",[219,426,428],{"class":221,"line":427},10,[219,429,430],{"class":237},"}\n",[156,432,433,434,437,438,441,442,445],{},"Notice the ",[198,435,436],{},"@ValidateIf"," on ",[198,439,440],{},"email",". Email is optional, but if someone provides one it has to be a real email. Without that guard, class-validator runs ",[198,443,444],{},"@IsEmail"," against empty strings and rejects perfectly valid requests where the user simply left the field blank. Small detail, but those are the details that make the difference between forms that feel helpful and forms that feel adversarial.",[156,447,448,449,452],{},"The ",[198,450,451],{},"each: true"," on the children array is another small thing worth flagging. It validates every UUID in the array, not just the first one. If someone sends in five children and the third one is malformed, you want to know about that one specifically.",[187,454,456],{"id":455},"_2-system-to-system-validation","2. System to system validation",[156,458,459],{},"This is validation for data coming from things that are not your users. Webhooks. Scraper output. Third-party APIs. Batch jobs.",[156,461,462,463,467,468,471],{},"Here is a fun one from the ",[203,464,98],{"href":465,"rel":466},"https://jobven.com",[207]," scraper. Each company in the system has a URL normalisation config that decides which query parameters to keep when deduplicating job URLs. The config accepts either the wildcard ",[198,469,470],{},"\"*\""," (keep everything) or an explicit array of parameter names.",[210,473,475],{"className":212,"code":474,"language":214,"meta":215,"style":215},"@ValidatorConstraint({ name: 'isArrayOrWildcard', async: false })\nexport class IsArrayOrWildcardConstraint\n  implements ValidatorConstraintInterface\n{\n  validate(value: any, _args: ValidationArguments) {\n    if (value === '*') {\n      return true;\n    }\n\n    if (Array.isArray(value)) {\n      return value.every((item) => typeof item === 'string');\n    }\n\n    return false;\n  }\n\n  defaultMessage(_args: ValidationArguments) {\n    return 'allowedQueryParams must be either an array of strings or the wildcard \"*\"';\n  }\n}\n",[198,476,477,516,525,533,538,568,593,603,608,612,635,678,683,688,698,704,709,728,742,747],{"__ignoreMap":215},[219,478,479,482,485,487,490,493,495,497,500,502,504,507,509,512,514],{"class":221,"line":222},[219,480,481],{"class":237},"@",[219,483,484],{"class":247},"ValidatorConstraint",[219,486,263],{"class":251},[219,488,489],{"class":237},"{",[219,491,492],{"class":331}," name",[219,494,386],{"class":237},[219,496,400],{"class":237},[219,498,499],{"class":371},"isArrayOrWildcard",[219,501,368],{"class":237},[219,503,377],{"class":237},[219,505,506],{"class":331}," async",[219,508,386],{"class":237},[219,510,511],{"class":389}," false",[219,513,408],{"class":237},[219,515,315],{"class":251},[219,517,518,520,522],{"class":221,"line":241},[219,519,226],{"class":225},[219,521,230],{"class":229},[219,523,524],{"class":233}," IsArrayOrWildcardConstraint\n",[219,526,527,530],{"class":221,"line":255},[219,528,529],{"class":229},"  implements",[219,531,532],{"class":233}," ValidatorConstraintInterface\n",[219,534,535],{"class":221,"line":318},[219,536,537],{"class":237},"{\n",[219,539,540,543,545,548,550,553,555,558,560,563,566],{"class":221,"line":328},[219,541,542],{"class":331},"  validate",[219,544,263],{"class":237},[219,546,547],{"class":266},"value",[219,549,386],{"class":237},[219,551,552],{"class":233}," any",[219,554,377],{"class":237},[219,556,557],{"class":266}," _args",[219,559,386],{"class":237},[219,561,562],{"class":233}," ValidationArguments",[219,564,565],{"class":237},")",[219,567,238],{"class":237},[219,569,570,573,576,578,581,583,586,588,591],{"class":221,"line":341},[219,571,572],{"class":225},"    if",[219,574,575],{"class":331}," (",[219,577,547],{"class":251},[219,579,580],{"class":237}," ===",[219,582,400],{"class":237},[219,584,585],{"class":371},"*",[219,587,368],{"class":237},[219,589,590],{"class":331},") ",[219,592,537],{"class":237},[219,594,595,598,600],{"class":221,"line":348},[219,596,597],{"class":225},"      return",[219,599,390],{"class":389},[219,601,602],{"class":237},";\n",[219,604,605],{"class":221,"line":358},[219,606,607],{"class":237},"    }\n",[219,609,610],{"class":221,"line":413},[219,611,345],{"emptyLinePlaceholder":344},[219,613,614,616,618,621,623,626,628,630,633],{"class":221,"line":427},[219,615,572],{"class":225},[219,617,575],{"class":331},[219,619,620],{"class":251},"Array",[219,622,276],{"class":237},[219,624,625],{"class":247},"isArray",[219,627,263],{"class":331},[219,629,547],{"class":251},[219,631,632],{"class":331},")) ",[219,634,537],{"class":237},[219,636,638,640,643,645,648,650,652,655,657,659,662,665,667,669,672,674,676],{"class":221,"line":637},11,[219,639,597],{"class":225},[219,641,642],{"class":251}," value",[219,644,276],{"class":237},[219,646,647],{"class":247},"every",[219,649,263],{"class":331},[219,651,263],{"class":237},[219,653,654],{"class":266},"item",[219,656,565],{"class":237},[219,658,270],{"class":229},[219,660,661],{"class":237}," typeof",[219,663,664],{"class":251}," item",[219,666,580],{"class":237},[219,668,400],{"class":237},[219,670,671],{"class":371},"string",[219,673,368],{"class":237},[219,675,565],{"class":331},[219,677,602],{"class":237},[219,679,681],{"class":221,"line":680},12,[219,682,607],{"class":237},[219,684,686],{"class":221,"line":685},13,[219,687,345],{"emptyLinePlaceholder":344},[219,689,691,694,696],{"class":221,"line":690},14,[219,692,693],{"class":225},"    return",[219,695,511],{"class":389},[219,697,602],{"class":237},[219,699,701],{"class":221,"line":700},15,[219,702,703],{"class":237},"  }\n",[219,705,707],{"class":221,"line":706},16,[219,708,345],{"emptyLinePlaceholder":344},[219,710,712,715,717,720,722,724,726],{"class":221,"line":711},17,[219,713,714],{"class":331},"  defaultMessage",[219,716,263],{"class":237},[219,718,719],{"class":266},"_args",[219,721,386],{"class":237},[219,723,562],{"class":233},[219,725,565],{"class":237},[219,727,238],{"class":237},[219,729,731,733,735,738,740],{"class":221,"line":730},18,[219,732,693],{"class":225},[219,734,400],{"class":237},[219,736,737],{"class":371},"allowedQueryParams must be either an array of strings or the wildcard \"*\"",[219,739,368],{"class":237},[219,741,602],{"class":237},[219,743,745],{"class":221,"line":744},19,[219,746,703],{"class":237},[219,748,750],{"class":221,"line":749},20,[219,751,430],{"class":237},[156,753,754,755,757],{},"The reason this is interesting is that the usual validator decorators do not compose well to express \"array of strings OR the literal string ",[198,756,585],{},"\". Writing a tiny custom constraint gets you exactly the shape you want, with a message that tells future-you (or a teammate) what the rule actually is.",[187,759,761],{"id":760},"_3-business-logic-validation","3. Business logic validation",[156,763,764],{},"This is the one that catches people. It is not \"is the input well formed\" but \"is this action allowed right now, given the state of the system\".",[156,766,767],{},"Think permissions. Trial expiration. Quota enforcement. Can this user delete this record? Has this subscription lapsed? Is this employee still active?",[156,769,770,771,775,776,779,780,783,784,787],{},"A small but good example from ",[203,772,74],{"href":773,"rel":774},"https://findchildcare.ca",[207]," is a custom decorator called ",[198,777,778],{},"@IsBiggerThan",". Listings have an age range: minimum age and maximum age the centre accepts. The rule is that ",[198,781,782],{},"maxAge"," has to be greater than or equal to ",[198,785,786],{},"minAge",". That is a business rule, not a formatting rule, and it cannot be expressed in the type system.",[210,789,791],{"className":212,"code":790,"language":214,"meta":215,"style":215},"export function IsBiggerThan(\n  property: string,\n  validationOptions?: IsBiggerThanOptions,\n) {\n  return function (object: object, propertyName: string) {\n    registerDecorator({\n      name: 'isBiggerThan',\n      target: object.constructor,\n      propertyName: propertyName,\n      constraints: [property],\n      options: validationOptions,\n      validator: {\n        validate(value: any, args: ValidationArguments) {\n          const [relatedPropertyName] = args.constraints;\n          const relatedValue = (args.object as any)[relatedPropertyName] || 0;\n\n          const bothNumbers =\n            typeof value === 'number' && typeof relatedValue === 'number';\n\n          if (validationOptions?.allowEqual) {\n            return bothNumbers && value >= relatedValue;\n          }\n\n          return bothNumbers && value > relatedValue;\n        },\n      },\n    });\n  };\n}\n",[198,792,793,806,818,830,836,866,875,891,907,918,936,948,957,983,1007,1047,1051,1061,1093,1097,1117,1136,1142,1147,1166,1172,1178,1188,1194],{"__ignoreMap":215},[219,794,795,797,800,803],{"class":221,"line":222},[219,796,226],{"class":225},[219,798,799],{"class":229}," function",[219,801,802],{"class":247}," IsBiggerThan",[219,804,805],{"class":237},"(\n",[219,807,808,811,813,815],{"class":221,"line":241},[219,809,810],{"class":266},"  property",[219,812,386],{"class":237},[219,814,421],{"class":233},[219,816,817],{"class":237},",\n",[219,819,820,823,825,828],{"class":221,"line":255},[219,821,822],{"class":266},"  validationOptions",[219,824,335],{"class":237},[219,826,827],{"class":233}," IsBiggerThanOptions",[219,829,817],{"class":237},[219,831,832,834],{"class":221,"line":318},[219,833,565],{"class":237},[219,835,238],{"class":237},[219,837,838,841,843,845,848,850,853,855,858,860,862,864],{"class":221,"line":328},[219,839,840],{"class":225},"  return",[219,842,799],{"class":229},[219,844,575],{"class":237},[219,846,847],{"class":266},"object",[219,849,386],{"class":237},[219,851,852],{"class":233}," object",[219,854,377],{"class":237},[219,856,857],{"class":266}," propertyName",[219,859,386],{"class":237},[219,861,421],{"class":233},[219,863,565],{"class":237},[219,865,238],{"class":237},[219,867,868,871,873],{"class":221,"line":341},[219,869,870],{"class":247},"    registerDecorator",[219,872,263],{"class":331},[219,874,537],{"class":237},[219,876,877,880,882,884,887,889],{"class":221,"line":348},[219,878,879],{"class":331},"      name",[219,881,386],{"class":237},[219,883,400],{"class":237},[219,885,886],{"class":371},"isBiggerThan",[219,888,368],{"class":237},[219,890,817],{"class":237},[219,892,893,896,898,900,902,905],{"class":221,"line":358},[219,894,895],{"class":331},"      target",[219,897,386],{"class":237},[219,899,852],{"class":251},[219,901,276],{"class":237},[219,903,904],{"class":251},"constructor",[219,906,817],{"class":237},[219,908,909,912,914,916],{"class":221,"line":413},[219,910,911],{"class":331},"      propertyName",[219,913,386],{"class":237},[219,915,857],{"class":251},[219,917,817],{"class":237},[219,919,920,923,925,928,931,934],{"class":221,"line":427},[219,921,922],{"class":331},"      constraints",[219,924,386],{"class":237},[219,926,927],{"class":331}," [",[219,929,930],{"class":251},"property",[219,932,933],{"class":331},"]",[219,935,817],{"class":237},[219,937,938,941,943,946],{"class":221,"line":637},[219,939,940],{"class":331},"      options",[219,942,386],{"class":237},[219,944,945],{"class":251}," validationOptions",[219,947,817],{"class":237},[219,949,950,953,955],{"class":221,"line":680},[219,951,952],{"class":331},"      validator",[219,954,386],{"class":237},[219,956,238],{"class":237},[219,958,959,962,964,966,968,970,972,975,977,979,981],{"class":221,"line":685},[219,960,961],{"class":331},"        validate",[219,963,263],{"class":237},[219,965,547],{"class":266},[219,967,386],{"class":237},[219,969,552],{"class":233},[219,971,377],{"class":237},[219,973,974],{"class":266}," args",[219,976,386],{"class":237},[219,978,562],{"class":233},[219,980,565],{"class":237},[219,982,238],{"class":237},[219,984,985,988,990,993,995,998,1000,1002,1005],{"class":221,"line":690},[219,986,987],{"class":229},"          const",[219,989,927],{"class":237},[219,991,992],{"class":251},"relatedPropertyName",[219,994,933],{"class":237},[219,996,997],{"class":237}," =",[219,999,974],{"class":251},[219,1001,276],{"class":237},[219,1003,1004],{"class":251},"constraints",[219,1006,602],{"class":237},[219,1008,1009,1011,1014,1016,1018,1021,1023,1025,1028,1030,1033,1035,1038,1041,1045],{"class":221,"line":700},[219,1010,987],{"class":229},[219,1012,1013],{"class":251}," relatedValue",[219,1015,997],{"class":237},[219,1017,575],{"class":331},[219,1019,1020],{"class":251},"args",[219,1022,276],{"class":237},[219,1024,847],{"class":251},[219,1026,1027],{"class":225}," as",[219,1029,552],{"class":233},[219,1031,1032],{"class":331},")[",[219,1034,992],{"class":251},[219,1036,1037],{"class":331},"] ",[219,1039,1040],{"class":237},"||",[219,1042,1044],{"class":1043},"sbssI"," 0",[219,1046,602],{"class":237},[219,1048,1049],{"class":221,"line":706},[219,1050,345],{"emptyLinePlaceholder":344},[219,1052,1053,1055,1058],{"class":221,"line":711},[219,1054,987],{"class":229},[219,1056,1057],{"class":251}," bothNumbers",[219,1059,1060],{"class":237}," =\n",[219,1062,1063,1066,1068,1070,1072,1075,1077,1079,1081,1083,1085,1087,1089,1091],{"class":221,"line":730},[219,1064,1065],{"class":237},"            typeof",[219,1067,642],{"class":251},[219,1069,580],{"class":237},[219,1071,400],{"class":237},[219,1073,1074],{"class":371},"number",[219,1076,368],{"class":237},[219,1078,288],{"class":237},[219,1080,661],{"class":237},[219,1082,1013],{"class":251},[219,1084,580],{"class":237},[219,1086,400],{"class":237},[219,1088,1074],{"class":371},[219,1090,368],{"class":237},[219,1092,602],{"class":237},[219,1094,1095],{"class":221,"line":744},[219,1096,345],{"emptyLinePlaceholder":344},[219,1098,1099,1102,1104,1107,1110,1113,1115],{"class":221,"line":749},[219,1100,1101],{"class":225},"          if",[219,1103,575],{"class":331},[219,1105,1106],{"class":251},"validationOptions",[219,1108,1109],{"class":237},"?.",[219,1111,1112],{"class":251},"allowEqual",[219,1114,590],{"class":331},[219,1116,537],{"class":237},[219,1118,1120,1123,1125,1127,1129,1132,1134],{"class":221,"line":1119},21,[219,1121,1122],{"class":225},"            return",[219,1124,1057],{"class":251},[219,1126,288],{"class":237},[219,1128,642],{"class":251},[219,1130,1131],{"class":237}," >=",[219,1133,1013],{"class":251},[219,1135,602],{"class":237},[219,1137,1139],{"class":221,"line":1138},22,[219,1140,1141],{"class":237},"          }\n",[219,1143,1145],{"class":221,"line":1144},23,[219,1146,345],{"emptyLinePlaceholder":344},[219,1148,1150,1153,1155,1157,1159,1162,1164],{"class":221,"line":1149},24,[219,1151,1152],{"class":225},"          return",[219,1154,1057],{"class":251},[219,1156,288],{"class":237},[219,1158,642],{"class":251},[219,1160,1161],{"class":237}," >",[219,1163,1013],{"class":251},[219,1165,602],{"class":237},[219,1167,1169],{"class":221,"line":1168},25,[219,1170,1171],{"class":237},"        },\n",[219,1173,1175],{"class":221,"line":1174},26,[219,1176,1177],{"class":237},"      },\n",[219,1179,1181,1184,1186],{"class":221,"line":1180},27,[219,1182,1183],{"class":237},"    }",[219,1185,565],{"class":331},[219,1187,602],{"class":237},[219,1189,1191],{"class":221,"line":1190},28,[219,1192,1193],{"class":237},"  };\n",[219,1195,1197],{"class":221,"line":1196},29,[219,1198,430],{"class":237},[156,1200,1201],{},"Now anywhere I need the constraint, it reads naturally:",[210,1203,1205],{"className":212,"code":1204,"language":214,"meta":215,"style":215},"@IsBiggerThan('minAge', { allowEqual: true })\nmaxAge: number\n",[198,1206,1207,1237],{"__ignoreMap":215},[219,1208,1209,1211,1214,1216,1218,1220,1222,1224,1226,1229,1231,1233,1235],{"class":221,"line":222},[219,1210,481],{"class":237},[219,1212,1213],{"class":247},"IsBiggerThan",[219,1215,263],{"class":251},[219,1217,368],{"class":237},[219,1219,786],{"class":371},[219,1221,368],{"class":237},[219,1223,377],{"class":237},[219,1225,380],{"class":237},[219,1227,1228],{"class":331}," allowEqual",[219,1230,386],{"class":237},[219,1232,390],{"class":389},[219,1234,408],{"class":237},[219,1236,315],{"class":251},[219,1238,1239,1241,1243],{"class":221,"line":241},[219,1240,782],{"class":233},[219,1242,386],{"class":237},[219,1244,1245],{"class":251}," number\n",[156,1247,1248],{},"Business rules as reusable decorators is one of those patterns I keep reaching for. It pushes the rule into a named thing you can apply everywhere the rule applies, and you stop writing one-off custom validators in the service layer that drift out of sync across endpoints.",[156,1250,1251],{},"Another flavour of business logic validation is deduplication before insert. KidzLog has a parent service that checks whether a parent with the same email already exists in the same centre before creating a new record. If a match is found, it merges the incoming children into the existing parent instead of creating a duplicate.",[156,1253,1254],{},"This is not validation in the class-validator sense. No decorator will do this for you. But it is validation in the deeper sense: you are confirming that the action is allowed given the current state of the system, and you are picking a sensible path when it is not.",[187,1256,1258],{"id":1257},"_4-ingestion-drift","4. Ingestion drift",[156,1260,1261],{},"This one deserves its own flavour because it is easy to miss, and scrapers live in the middle of it.",[156,1263,1264,1265,1268],{},"When you ingest data from external sources, the data that validates cleanly today may not validate cleanly in six months. Sites rework their markup. APIs deprecate fields. Vendors silently change response shapes, or start returning ",[198,1266,1267],{},"null"," where they used to return strings. The data you stored three months ago may no longer match the data you are ingesting today.",[156,1270,1271],{},"Scrapers are the clearest example because the HTML you parse is not a contract. It is a page rendered for a human, and the owner of that page owes you nothing. They can change class names, reorder fields, move the job description to a different component, or A/B test two layouts and return a different one on every request. Your selectors silently start matching the wrong thing, and your pipeline happily writes \"Apply now\" into your description column for a week before anyone notices.",[156,1273,1274],{},"The only defence is to treat ingestion boundaries as validation boundaries, even for systems you built. A few things I do in the Jobven scraper:",[1276,1277,1278,1286,1304,1310],"ul",{},[1279,1280,1281,1285],"li",{},[1282,1283,1284],"strong",{},"Validate the shape, not just the presence."," A successful fetch is not a successful scrape. After parsing, check that the fields you expect are there and look roughly right (job title under N characters, description over N characters, URL matches a domain pattern). Anything that does not pass goes to a review queue, not to the database.",[1279,1287,1288,1291,1292,1295,1296,1299,1300,1303],{},[1282,1289,1290],{},"Count attempts, flag the persistent failures."," Each job has a ",[198,1293,1294],{},"scrapeAttemptCount"," and a ",[198,1297,1298],{},"maxScrapeAttempts",". If the same job fails to validate more times than allowed, it gets a ",[198,1301,1302],{},"markManualReview"," flag and stops retrying. You want the system to stop repeating its mistake, and you want a human to see the pile.",[1279,1305,1306,1309],{},[1282,1307,1308],{},"Alert on the rate of drift, not individual failures."," A single failed scrape is noise. A spike in failures for a specific company is usually a signal that their site changed. I publish failure events to a consumer that aggregates by company and pings Discord when the rate jumps.",[1279,1311,1312,1315],{},[1282,1313,1314],{},"Log the raw shape."," Before parsing, log what came in. Not the full content necessarily, but enough to reconstruct what the page looked like when the scrape broke. Drift debugging without the original payload is archaeology.",[156,1317,1318],{},"The same thinking applies to any ingestion boundary. Third-party webhooks, partner APIs, CSV uploads from a client who \"cleaned up\" their spreadsheet before sending it to you. If you did not author the producer, assume the producer will change. Validate on the way in.",[169,1320,1322],{"id":1321},"where-should-i-validate","Where should I validate?",[156,1324,1325],{},"This is the question that comes up most, and I expect it to come up more now that people are prompting their way to full apps with LLMs. I keep getting asked \"should I validate on the frontend or the backend\".",[156,1327,1328],{},"The answer is yes. Both. Also the database. They solve different problems.",[187,1330,1332],{"id":1331},"frontend-validation-is-for-ux","Frontend validation is for UX",[156,1334,1335],{},"The frontend validates so your users know immediately when something is wrong. A red \"password must be 8 characters\" before they hit submit is worth more than a backend 400 response three seconds later. Frontend validation is about keeping the experience tight.",[156,1337,1338],{},"It is also a layer you cannot trust. The browser is a hostile environment. Anyone can disable your JavaScript, modify the request, or hit the API directly. Frontend validation is for the user, not for your data integrity.",[187,1340,1342],{"id":1341},"backend-validation-is-for-integrity","Backend validation is for integrity",[156,1344,1345],{},"The backend validates so your data stays coherent. The backend does not trust the frontend. It does not trust other services. It definitely does not trust request bodies that showed up at 3am from an IP in a country you have never shipped to.",[156,1347,1348],{},"Backend validation is where business rules get enforced. The frontend can pretend a trial ended, but the backend is the only place that actually makes that true.",[187,1350,1352],{"id":1351},"the-database-is-the-last-line-of-defence","The database is the last line of defence",[156,1354,1355,1356,1359,1360,1365,1366,276],{},"And then the database is where you put the constraints you would lose sleep over if they were violated. ",[198,1357,1358],{},"NOT NULL",". Foreign keys. Unique indexes. ",[203,1361,1364],{"href":1362,"rel":1363},"https://oliha.dev/articles/using-check-constraints-in-postgresql-for-value-validation/",[207],"Check constraints",". ",[203,1367,1370],{"href":1368,"rel":1369},"https://oliha.dev/articles/enums-as-arrays-in-postgresql/",[207],"Enums",[156,1372,1373],{},"Everything above the database is code, and code has bugs. The database is the one layer that catches everything, even the bugs in your validation. If a column should never be null, tell the database. If a value must come from a specific set, tell the database. Belt and braces.",[169,1375,1377],{"id":1376},"custom-validators-as-named-rules","Custom validators as named rules",[156,1379,1380],{},"If you take one thing from all of this, let it be this: treat custom validators as a first-class way to express business rules.",[156,1382,1383,1384,1386,1387,1390],{},"Most teams default to putting rules in services or controllers. Then the same rule shows up in three places, drifts in two of them, and the bug you ship is the case where the drift matters. A named decorator like ",[198,1385,778],{}," or ",[198,1388,1389],{},"@IsUniqueEmail"," turns the rule into a single source of truth you can apply anywhere it belongs.",[156,1392,1393,1394,1397],{},"This is not a NestJS thing or a class-validator thing. The same pattern works in Zod with ",[198,1395,1396],{},".refine()",", in Yup with custom tests, in Vuelidate with custom validators. The point is that the rule has a name, lives in one place, and shows up in your code where the rule applies.",[169,1399,1401],{"id":1400},"closing","Closing",[156,1403,1404],{},"Validation is not about rejecting data. It is about knowing what shape your data is in at every boundary in your system.",[156,1406,1407],{},"Every boundary is different. User input needs one kind of vigilance. System to system traffic needs another. Business rules need a third. And ingestion drift needs the kind of vigilance that does not get written until something has already broken, so it is worth writing early.",[156,1409,1410],{},"The frameworks and libraries do not matter much. What matters is that you treat validation as a real concern, give it names, put it where it belongs, and resist the temptation to let one layer pick up the slack for another. They all have jobs to do.",[156,1412,1413],{},"Next up, I will write about error handling, which is the partner to validation. Validation tells you what should not happen. Error handling tells you what to do when it happens anyway.",[1415,1416],"blog-cta",{},[1418,1419,1420],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":215,"searchDepth":241,"depth":241,"links":1422},[1423,1424,1430,1435,1436],{"id":171,"depth":241,"text":172},{"id":184,"depth":241,"text":185,"children":1425},[1426,1427,1428,1429],{"id":189,"depth":255,"text":190},{"id":455,"depth":255,"text":456},{"id":760,"depth":255,"text":761},{"id":1257,"depth":255,"text":1258},{"id":1321,"depth":241,"text":1322,"children":1431},[1432,1433,1434],{"id":1331,"depth":255,"text":1332},{"id":1341,"depth":255,"text":1342},{"id":1351,"depth":255,"text":1352},{"id":1376,"depth":241,"text":1377},{"id":1400,"depth":241,"text":1401},[7],"2026-04-18T00:00:00.000Z","Validation is one of those topics that feels boring until something goes wrong in production. Here is how I think about validation across the stack, with real examples from the apps I run.","md",null,"/assets/images/blog/validation-across-the-stack.webp",{},{"title":14,"description":1439},{"loc":15},[7,1447,1448,1449],"validation","engineering","typescript","qZBpt7Mp9b1FDHai7x_I_zR0TtdlazGF8_RZx2ANJUY",[1452,1454],{"title":10,"path":11,"stem":12,"description":1453,"children":-1},"A practical guide to Claude Code's plugin system - hooks, skills, and commands. I built Claude Overflow to learn how it all fits together.",{"title":22,"path":23,"stem":24,"description":1455,"children":-1},"Learn three effective methods to handle cron jobs in a multi-instance NestJS environment. From named instances to database locking strategies.",[1457,1470,1481,1490,1502,1513,1524,1536,1547,1558,1570,1582,1593,1604,1614,1624,1634,1645,1656,1666,1677,1687,1698,1708,1719,1729,1740],{"id":1458,"color":1459,"description":1460,"extension":1461,"meta":1462,"name":1465,"slug":1466,"stem":1468,"__hash__":1469},"tags/tags/ai.yml","purple","Artificial Intelligence and machine learning topics","yml",{"path":1463,"body":1464,"title":1467},"/tags/ai",{"name":1465,"slug":1466,"description":1460,"color":1459},"AI","ai","Ai","tags/ai","G7CnVSq80rIqg50l36ELmcNhZNBz-H2_aLo5yDVzAqM",{"id":1471,"color":1472,"description":1473,"extension":1461,"meta":1474,"name":1477,"slug":1478,"stem":1479,"__hash__":1480},"tags/tags/angular.yml","red","Angular framework tutorials and component development",{"path":1475,"body":1476,"title":1477},"/tags/angular",{"name":1477,"slug":1478,"description":1473,"color":1472},"Angular","angular","tags/angular","Mbg1PXJtnm00wtkZDFKwU88QR8l3H9uwx7OzmaZOzlE",{"id":1482,"color":1483,"description":1484,"extension":1461,"meta":1485,"name":5,"slug":7,"stem":1488,"__hash__":1489},"tags/tags/articles.yml","gray","General articles and blog posts",{"path":1486,"body":1487,"title":5},"/tags/articles",{"name":5,"slug":7,"description":1484,"color":1483},"tags/articles","3As8iuc9uTKMAqYAPL8vNWMdxJgiSyarF3BaIGW1GJo",{"id":1491,"color":1492,"description":1493,"extension":1461,"meta":1494,"name":1497,"slug":1498,"stem":1500,"__hash__":1501},"tags/tags/aws.yml","orange","Amazon Web Services cloud platform tutorials and integrations",{"path":1495,"body":1496,"title":1499},"/tags/aws",{"name":1497,"slug":1498,"description":1493,"color":1492},"AWS","aws","Aws","tags/aws","vBDD4wRAJaga8aSV1FRSvXWhL-FmbWZoVEDlwGlayp0",{"id":1503,"color":1504,"description":1505,"extension":1461,"meta":1506,"name":1509,"slug":1510,"stem":1511,"__hash__":1512},"tags/tags/bard.yml","blue","Google Bard AI assistant",{"path":1507,"body":1508,"title":1509},"/tags/bard",{"name":1509,"slug":1510,"description":1505,"color":1504},"Bard","bard","tags/bard","GRfrXpxgLeT3In8wyRY_tKs6J5r2oycvTarT32SFX24",{"id":1514,"color":1472,"description":1515,"extension":1461,"meta":1516,"name":1519,"slug":1520,"stem":1522,"__hash__":1523},"tags/tags/building-maplestack.yml","Series on building the MapleStack platform",{"path":1517,"body":1518,"title":1521},"/tags/building-maplestack",{"name":1519,"slug":1520,"description":1515,"color":1472},"Building MapleStack","building-maplestack","Building Maplestack","tags/building-maplestack","XREjt6NTzGKj8T1NOBmnW_mU5N52IPSOHoun5BYgLJM",{"id":1525,"color":1526,"description":1527,"extension":1461,"meta":1528,"name":1531,"slug":1532,"stem":1534,"__hash__":1535},"tags/tags/chatgpt.yml","green","OpenAI ChatGPT integration and usage",{"path":1529,"body":1530,"title":1533},"/tags/chatgpt",{"name":1531,"slug":1532,"description":1527,"color":1526},"ChatGPT","chatgpt","Chatgpt","tags/chatgpt","itxY7tNXwu-_GEifox8PRdE_0g1Rf6Z3hbiYtXyEG9I",{"id":1537,"color":1492,"description":1538,"extension":1461,"meta":1539,"name":1542,"slug":1543,"stem":1545,"__hash__":1546},"tags/tags/dataloader.yml","DataLoader utility for batching and caching",{"path":1540,"body":1541,"title":1544},"/tags/dataloader",{"name":1542,"slug":1543,"description":1538,"color":1492},"DataLoader","dataloader","Dataloader","tags/dataloader","4CSnb5b488RVmvccjwlAcouUgMqsL4uqsYMKYAVmw9w",{"id":1548,"color":1504,"description":1549,"extension":1461,"meta":1550,"name":1553,"slug":1554,"stem":1556,"__hash__":1557},"tags/tags/dns.yml","Domain Name System configuration and troubleshooting",{"path":1551,"body":1552,"title":1555},"/tags/dns",{"name":1553,"slug":1554,"description":1549,"color":1504},"DNS","dns","Dns","tags/dns","cbHEqsx5H9sRmFfsu9dNf8s17ZFepj6xJ2aBwbYcNDU",{"id":1559,"color":1560,"description":1561,"extension":1461,"meta":1562,"name":1565,"slug":1566,"stem":1568,"__hash__":1569},"tags/tags/graphql.yml","pink","GraphQL API development, schema design, and implementation guides",{"path":1563,"body":1564,"title":1567},"/tags/graphql",{"name":1565,"slug":1566,"description":1561,"color":1560},"GraphQL","graphql","Graphql","tags/graphql","5S6782BsJqw_5ffa4j6WAyElUUyfi_thH_RTeqjwzm4",{"id":1571,"color":1572,"description":1573,"extension":1461,"meta":1574,"name":1577,"slug":1578,"stem":1580,"__hash__":1581},"tags/tags/javascript.yml","yellow","JavaScript programming language tutorials and best practices",{"path":1575,"body":1576,"title":1579},"/tags/javascript",{"name":1577,"slug":1578,"description":1573,"color":1572},"JavaScript","javascript","Javascript","tags/javascript","G6WEjmiQNrzkMmy6c3KupGB8yclr4-VXvgNkZzls7_w",{"id":1583,"color":1483,"description":1584,"extension":1461,"meta":1585,"name":1588,"slug":1589,"stem":1591,"__hash__":1592},"tags/tags/koajs.yml","Koa.js web framework for Node.js",{"path":1586,"body":1587,"title":1590},"/tags/koajs",{"name":1588,"slug":1589,"description":1584,"color":1483},"KoaJS","koajs","Koajs","tags/koajs","oF80IQqS8UyrcBKa_451xyKC_sZAD9SZdJmRbmSgbIc",{"id":1594,"color":1526,"description":1595,"extension":1461,"meta":1596,"name":1599,"slug":1600,"stem":1602,"__hash__":1603},"tags/tags/letsencrypt.yml","Free SSL/TLS certificates with Let's Encrypt",{"path":1597,"body":1598,"title":1601},"/tags/letsencrypt",{"name":1599,"slug":1600,"description":1595,"color":1526},"Let's Encrypt","letsencrypt","Letsencrypt","tags/letsencrypt","kzMKw4pJxlpJTCa7GwJsuovctznurD_6ucowl7ocvW4",{"id":1605,"color":1483,"description":1606,"extension":1461,"meta":1607,"name":1610,"slug":1611,"stem":1612,"__hash__":1613},"tags/tags/linux.yml","Linux server administration and command-line tools",{"path":1608,"body":1609,"title":1610},"/tags/linux",{"name":1610,"slug":1611,"description":1606,"color":1483},"Linux","linux","tags/linux","yAjXmKPJfSTqBD5mvCbDZ0-o22P-ouYfzezRr4qzt-Y",{"id":1615,"color":1472,"description":1616,"extension":1461,"meta":1617,"name":106,"slug":1620,"stem":1622,"__hash__":1623},"tags/tags/maplestack.yml","Building MapleStack job board platform",{"path":1618,"body":1619,"title":1621},"/tags/maplestack",{"name":106,"slug":1620,"description":1616,"color":1472},"maplestack","Maplestack","tags/maplestack","hbCEUc39DYE7ER0AMwnPCtuZ98sz6Q01u-vrdYE8Qw4",{"id":1625,"color":1492,"description":1626,"extension":1461,"meta":1627,"name":1630,"slug":1631,"stem":1632,"__hash__":1633},"tags/tags/migration.yml","Platform and data migration guides",{"path":1628,"body":1629,"title":1630},"/tags/migration",{"name":1630,"slug":1631,"description":1626,"color":1492},"Migration","migration","tags/migration","JBYhOnvqqL3x_Y0Wrfyl2cQ58frVtarlAKZeph326fg",{"id":1635,"color":1472,"description":1636,"extension":1461,"meta":1637,"name":1640,"slug":1641,"stem":1643,"__hash__":1644},"tags/tags/nestjs.yml","Articles about NestJS framework for building efficient, scalable Node.js server-side applications",{"path":1638,"body":1639,"title":1642},"/tags/nestjs",{"name":1640,"slug":1641,"description":1636,"color":1472},"NestJS","nestjs","Nestjs","tags/nestjs","BJe0bHCwWhZwqndup2DXtc0qzFZgmuY_93lFDVr0XSk",{"id":1646,"color":1526,"description":1647,"extension":1461,"meta":1648,"name":1651,"slug":1652,"stem":1654,"__hash__":1655},"tags/tags/nodejs.yml","Node.js runtime environment and server-side JavaScript",{"path":1649,"body":1650,"title":1653},"/tags/nodejs",{"name":1651,"slug":1652,"description":1647,"color":1526},"Node.js","nodejs","Nodejs","tags/nodejs","fHQ6WwidjZFONZA8Ebep5Ieejj4vuzYQBSqBImWWNfM",{"id":1657,"color":1526,"description":1658,"extension":1461,"meta":1659,"name":1662,"slug":1663,"stem":1664,"__hash__":1665},"tags/tags/nuxt.yml","Nuxt.js framework for Vue.js applications",{"path":1660,"body":1661,"title":1662},"/tags/nuxt",{"name":1662,"slug":1663,"description":1658,"color":1526},"Nuxt","nuxt","tags/nuxt","rNPsvKK-54iD_caiZWbujM6GPlMj540aK5dj-FGcOnA",{"id":1667,"color":1504,"description":1668,"extension":1461,"meta":1669,"name":1672,"slug":1673,"stem":1675,"__hash__":1676},"tags/tags/postgresql.yml","PostgreSQL database tutorials, tips, and best practices",{"path":1670,"body":1671,"title":1674},"/tags/postgresql",{"name":1672,"slug":1673,"description":1668,"color":1504},"PostgreSQL","postgresql","Postgresql","tags/postgresql","y22Ixmqt78PuX6EiFpfqvBF4uqrhpig2qitP9cyca5E",{"id":1678,"color":1459,"description":1679,"extension":1461,"meta":1680,"name":1683,"slug":1684,"stem":1685,"__hash__":1686},"tags/tags/projects.yml","Articles about personal and professional projects",{"path":1681,"body":1682,"title":1683},"/tags/projects",{"name":1683,"slug":1684,"description":1679,"color":1459},"Projects","projects","tags/projects","A5PSSJfGUKIH_WZ2H1VMzAATPihg8nAr8wWeTQeBri4",{"id":1688,"color":1689,"description":1690,"extension":1461,"meta":1691,"name":1694,"slug":1695,"stem":1696,"__hash__":1697},"tags/tags/react.yml","cyan","React library guides for building user interfaces",{"path":1692,"body":1693,"title":1694},"/tags/react",{"name":1694,"slug":1695,"description":1690,"color":1689},"React","react","tags/react","5ZYjto6-5PEILzin2j4JzeEcckTPq5Wtvusdt3Lkk-w",{"id":1699,"color":1504,"description":1700,"extension":1461,"meta":1701,"name":1704,"slug":1705,"stem":1706,"__hash__":1707},"tags/tags/stellar.yml","Stellar blockchain network and cryptocurrency development",{"path":1702,"body":1703,"title":1704},"/tags/stellar",{"name":1704,"slug":1705,"description":1700,"color":1504},"Stellar","stellar","tags/stellar","R1NkyZVdHyRvYo_v9ZGutJqJnLmFZnDp5A_CUtcqlBE",{"id":1709,"color":1689,"description":1710,"extension":1461,"meta":1711,"name":1714,"slug":1715,"stem":1717,"__hash__":1718},"tags/tags/tailwindcss.yml","Tailwind CSS utility-first styling framework",{"path":1712,"body":1713,"title":1716},"/tags/tailwindcss",{"name":1714,"slug":1715,"description":1710,"color":1689},"Tailwind CSS","tailwindcss","Tailwindcss","tags/tailwindcss","WRWTuri9vhdVQsXm-4l3xTfOZ71U8gDDrLIy8NkZs3o",{"id":1720,"color":1504,"description":1721,"extension":1461,"meta":1722,"name":1725,"slug":1449,"stem":1727,"__hash__":1728},"tags/tags/typescript.yml","TypeScript language features, type safety, and development tips",{"path":1723,"body":1724,"title":1726},"/tags/typescript",{"name":1725,"slug":1449,"description":1721,"color":1504},"TypeScript","Typescript","tags/typescript","lxEPW4jYk_r-Pu0Ku-cIpNz1xE8oLLy-8OZR9WLCMQI",{"id":1730,"color":1526,"description":1731,"extension":1461,"meta":1732,"name":1735,"slug":1736,"stem":1738,"__hash__":1739},"tags/tags/vps.yml","Virtual Private Server setup and management",{"path":1733,"body":1734,"title":1737},"/tags/vps",{"name":1735,"slug":1736,"description":1731,"color":1526},"VPS","vps","Vps","tags/vps","TXmItNVJMD2RmIe8GGMINzUACcLuqrwDhNfKlJj1s3U",{"id":1741,"color":1504,"description":1742,"extension":1461,"meta":1743,"name":1746,"slug":1747,"stem":1749,"__hash__":1750},"tags/tags/wordpress.yml","WordPress CMS and related topics",{"path":1744,"body":1745,"title":1748},"/tags/wordpress",{"name":1746,"slug":1747,"description":1742,"color":1504},"WordPress","wordpress","Wordpress","tags/wordpress","uipd6WrBu8NcNN3gwhWIDhEQZHU8XeU630qc0TOhUi4",1776556846401]