[{"data":1,"prerenderedAt":551},["ShallowReactive",2],{"tag-mock":3},[4],{"id":5,"title":6,"body":7,"categories":530,"coverImage":532,"date":533,"description":534,"extension":535,"meta":536,"navigation":164,"path":537,"seo":538,"stem":539,"sticky":164,"tags":540,"__hash__":550},"articles/crudio-da-una-spec-openapi-a-un-backend-mock-stateful.md","Crudio: da una spec OpenAPI a un backend mock stateful con un comando",{"type":8,"value":9,"toc":523},"minimark",[10,22,27,30,37,42,45,69,72,85,89,92,95,123,126,221,225,236,386,393,435,438,442,445,455,461,464,468,483,486,512,519],[11,12,13],"p",{},[14,15,16,17,21],"em",{},"In questo post voglio raccontarti perché ho costruito ",[18,19,20],"strong",{},"Crudio"," e il problema preciso che risolve: avere un backend finto che però si comporta come un backend vero, derivandolo interamente dalla tua spec OpenAPI.",[11,23,24],{},[14,25,26],{},"Non è l'ennesimo mock server che restituisce risposte preconfezionate. È un backend che ricorda cosa ci hai scritto dentro.",[11,28,29],{},"Chiunque sviluppi frontend conosce la stessa frustrazione: ti serve un backend per lavorare, ma quello vero non è pronto, è instabile, o semplicemente non vuoi dipenderci durante i test. Così ripieghi su un mock server.",[11,31,32,33,36],{},"E qui scatta il problema. ",[18,34,35],{},"I mock server ti costringono a una scelta che non dovresti dover fare",": o sono guidati dalla spec, oppure sono stateful. Mai entrambe le cose.",[38,39,41],"h2",{"id":40},"il-buco-che-nessuno-riempie","Il buco che nessuno riempie",[11,43,44],{},"Mettiamola così, con gli strumenti che probabilmente già usi:",[46,47,48,59],"ul",{},[49,50,51,54,55,58],"li",{},[18,52,53],{},"Prism"," legge la tua spec OpenAPI ed è fedele al contratto, ma è ",[14,56,57],{},"stateless",". Fai una POST e il dato sparisce alla GET successiva. Ottimo per uno smoke test, inutile quando devi verificare un flusso reale.",[49,60,61,64,65,68],{},[18,62,63],{},"json-server"," è stateful e ti dà CRUD persistente, ma ",[18,66,67],{},"ignora completamente la tua spec"," e non valida nulla. Accetta felicemente payload che il tuo backend vero rifiuterebbe a priori.",[11,70,71],{},"Il risultato è che, quando devo testare sul serio come il frontend gestisce paginazione, errori di validazione, 404 e update parziali, nessuno dei due basta.",[11,73,74,84],{},[18,75,76,77,80,81,83],{},"Crudio nasce esattamente per riempire quella casella vuota: spec-driven ",[14,78,79],{},"e"," stateful ",[14,82,79],{}," con validazione."," Tutto derivato dal tuo contratto OpenAPI, niente scritto a mano.",[38,86,88],{"id":87},"cosa-fa-in-pratica","Cosa fa, in pratica",[11,90,91],{},"Crudio legge una spec OpenAPI 3.x e tira su un'API mock funzionante con persistenza. Gli endpoint CRUD si comportano come un piccolo backend reale; gli endpoint non-CRUD mantengono uno stato per-operazione, così l'intera spec è servibile da un unico runtime.",[11,93,94],{},"Nel concreto:",[46,96,97,103,110,117,120],{},[49,98,99,100],{},"i request body CRUD sono ",[18,101,102],{},"validati contro il tuo schema",[49,104,105,106,109],{},"i dati ",[18,107,108],{},"persistono tra una chiamata e l'altra"," (file JSON, nessun database da configurare)",[49,111,112,113,116],{},"gli ",[18,114,115],{},"ID sono generati in base alla spec"," (interi, UUID, ecc.)",[49,118,119],{},"le route CRUD condividono lo stato della risorsa",[49,121,122],{},"le route non-CRUD usano uno stato di operazione persistito",[11,124,125],{},"E si avvia con un solo comando:",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-bash shiki shiki-themes github-light github-dark","# Contro qualsiasi spec OpenAPI 3.x\nnpx @enricodeleo/crudio ./openapi.yaml\n\n# Con dati fake\nnpx @enricodeleo/crudio ./openapi.yaml --seed 10\n\n# Porta e storage personalizzati\nnpx @enricodeleo/crudio ./openapi.yaml --port 8080 --data-dir /tmp/data\n","bash","",[134,135,136,145,159,166,172,189,194,200],"code",{"__ignoreMap":132},[137,138,141],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"sJ8bj","# Contro qualsiasi spec OpenAPI 3.x\n",[137,146,148,152,156],{"class":139,"line":147},2,[137,149,151],{"class":150},"sScJk","npx",[137,153,155],{"class":154},"sZZnC"," @enricodeleo/crudio",[137,157,158],{"class":154}," ./openapi.yaml\n",[137,160,162],{"class":139,"line":161},3,[137,163,165],{"emptyLinePlaceholder":164},true,"\n",[137,167,169],{"class":139,"line":168},4,[137,170,171],{"class":143},"# Con dati fake\n",[137,173,175,177,179,182,186],{"class":139,"line":174},5,[137,176,151],{"class":150},[137,178,155],{"class":154},[137,180,181],{"class":154}," ./openapi.yaml",[137,183,185],{"class":184},"sj4cs"," --seed",[137,187,188],{"class":184}," 10\n",[137,190,192],{"class":139,"line":191},6,[137,193,165],{"emptyLinePlaceholder":164},[137,195,197],{"class":139,"line":196},7,[137,198,199],{"class":143},"# Porta e storage personalizzati\n",[137,201,203,205,207,209,212,215,218],{"class":139,"line":202},8,[137,204,151],{"class":150},[137,206,155],{"class":154},[137,208,181],{"class":154},[137,210,211],{"class":184}," --port",[137,213,214],{"class":184}," 8080",[137,216,217],{"class":184}," --data-dir",[137,219,220],{"class":154}," /tmp/data\n",[38,222,224],{"id":223},"si-comporta-come-un-backend-vero","Si comporta come un backend vero",[11,226,227,228,231,232,235],{},"Data una spec CRUD standard con path tipo ",[134,229,230],{},"/pets"," e ",[134,233,234],{},"/pets/{petId}",", ottieni un backend che fa quello che ti aspetti:",[127,237,239],{"className":129,"code":238,"language":131,"meta":132,"style":132},"# Create\ncurl -X POST http://localhost:3000/pets \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"name\":\"Rex\",\"tag\":\"dog\"}'\n# → 201 {\"id\":1,\"name\":\"Rex\",\"tag\":\"dog\"}\n\n# Get by ID — il dato è ancora lì, persistito su disco\ncurl http://localhost:3000/pets/1\n# → 200 {\"id\":1,\"name\":\"Rex\",\"tag\":\"dog\"}\n\n# Partial update\ncurl -X PATCH http://localhost:3000/pets/1 \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"tag\":\"cat\"}'\n# → 200 {\"id\":1,\"name\":\"Rex\",\"tag\":\"cat\"}\n\n# Delete\ncurl -X DELETE http://localhost:3000/pets/1\n# → 204\n",[134,240,241,246,263,273,281,286,290,295,302,308,313,319,334,343,351,357,362,368,380],{"__ignoreMap":132},[137,242,243],{"class":139,"line":140},[137,244,245],{"class":143},"# Create\n",[137,247,248,251,254,257,260],{"class":139,"line":147},[137,249,250],{"class":150},"curl",[137,252,253],{"class":184}," -X",[137,255,256],{"class":154}," POST",[137,258,259],{"class":154}," http://localhost:3000/pets",[137,261,262],{"class":184}," \\\n",[137,264,265,268,271],{"class":139,"line":161},[137,266,267],{"class":184},"  -H",[137,269,270],{"class":154}," 'Content-Type: application/json'",[137,272,262],{"class":184},[137,274,275,278],{"class":139,"line":168},[137,276,277],{"class":184},"  -d",[137,279,280],{"class":154}," '{\"name\":\"Rex\",\"tag\":\"dog\"}'\n",[137,282,283],{"class":139,"line":174},[137,284,285],{"class":143},"# → 201 {\"id\":1,\"name\":\"Rex\",\"tag\":\"dog\"}\n",[137,287,288],{"class":139,"line":191},[137,289,165],{"emptyLinePlaceholder":164},[137,291,292],{"class":139,"line":196},[137,293,294],{"class":143},"# Get by ID — il dato è ancora lì, persistito su disco\n",[137,296,297,299],{"class":139,"line":202},[137,298,250],{"class":150},[137,300,301],{"class":154}," http://localhost:3000/pets/1\n",[137,303,305],{"class":139,"line":304},9,[137,306,307],{"class":143},"# → 200 {\"id\":1,\"name\":\"Rex\",\"tag\":\"dog\"}\n",[137,309,311],{"class":139,"line":310},10,[137,312,165],{"emptyLinePlaceholder":164},[137,314,316],{"class":139,"line":315},11,[137,317,318],{"class":143},"# Partial update\n",[137,320,322,324,326,329,332],{"class":139,"line":321},12,[137,323,250],{"class":150},[137,325,253],{"class":184},[137,327,328],{"class":154}," PATCH",[137,330,331],{"class":154}," http://localhost:3000/pets/1",[137,333,262],{"class":184},[137,335,337,339,341],{"class":139,"line":336},13,[137,338,267],{"class":184},[137,340,270],{"class":154},[137,342,262],{"class":184},[137,344,346,348],{"class":139,"line":345},14,[137,347,277],{"class":184},[137,349,350],{"class":154}," '{\"tag\":\"cat\"}'\n",[137,352,354],{"class":139,"line":353},15,[137,355,356],{"class":143},"# → 200 {\"id\":1,\"name\":\"Rex\",\"tag\":\"cat\"}\n",[137,358,360],{"class":139,"line":359},16,[137,361,165],{"emptyLinePlaceholder":164},[137,363,365],{"class":139,"line":364},17,[137,366,367],{"class":143},"# Delete\n",[137,369,371,373,375,378],{"class":139,"line":370},18,[137,372,250],{"class":150},[137,374,253],{"class":184},[137,376,377],{"class":154}," DELETE",[137,379,301],{"class":154},[137,381,383],{"class":139,"line":382},19,[137,384,385],{"class":143},"# → 204\n",[11,387,388,389,392],{},"E soprattutto ",[18,390,391],{},"rifiuta ciò che il tuo backend vero rifiuterebbe",", perché valida contro lo schema:",[127,394,396],{"className":129,"code":395,"language":131,"meta":132,"style":132},"curl -X POST http://localhost:3000/pets \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"tag\":\"dog\"}'\n# → 400 {\"error\":\"Validation failed\",\"details\":[...]}\n# rifiutato: `name` è obbligatorio nel tuo schema\n",[134,397,398,410,418,425,430],{"__ignoreMap":132},[137,399,400,402,404,406,408],{"class":139,"line":140},[137,401,250],{"class":150},[137,403,253],{"class":184},[137,405,256],{"class":154},[137,407,259],{"class":154},[137,409,262],{"class":184},[137,411,412,414,416],{"class":139,"line":147},[137,413,267],{"class":184},[137,415,270],{"class":154},[137,417,262],{"class":184},[137,419,420,422],{"class":139,"line":161},[137,421,277],{"class":184},[137,423,424],{"class":154}," '{\"tag\":\"dog\"}'\n",[137,426,427],{"class":139,"line":168},[137,428,429],{"class":143},"# → 400 {\"error\":\"Validation failed\",\"details\":[...]}\n",[137,431,432],{"class":139,"line":174},[137,433,434],{"class":143},"# rifiutato: `name` è obbligatorio nel tuo schema\n",[11,436,437],{},"Questa è la differenza che conta: non stai testando contro risposte finte, stai testando contro il tuo contratto.",[38,439,441],{"id":440},"per-cosa-usarlo-e-per-cosa-no","Per cosa usarlo (e per cosa no)",[11,443,444],{},"Voglio essere onesto sui confini, perché è la parte che di solito viene nascosta.",[11,446,447,450,451,454],{},[18,448,449],{},"Usalo per:"," test di integrazione, sviluppo frontend, prototipazione di API, verifica del contratto. È costruito su ",[18,452,453],{},"Express 5 + ajv",", persiste su file JSON, ed è pensato per girare ovunque senza dipendenze pesanti.",[11,456,457,460],{},[18,458,459],{},"Non usarlo per:"," backend di produzione, load testing, o qualunque cosa richieda business logic specifica di dominio senza handler custom. Crudio non inventa logica che non sia nel contratto.",[11,462,463],{},"Allo stato attuale lo stato è per-risorsa: relazioni, foreign key e autenticazione sono le domande ovvie successive, ed è esattamente lì che mi piacerebbe capire dove andare a tracciare la linea del \"comportarsi come un backend vero\".",[38,465,467],{"id":466},"provalo","Provalo",[11,469,470,471,474,475,482],{},"Crudio è ",[18,472,473],{},"open source, licenza MIT",", e lo trovi su GitHub: ",[476,477,481],"a",{"href":478,"rel":479},"https://github.com/enricodeleo/crudio",[480],"nofollow","github.com/enricodeleo/crudio",".",[11,484,485],{},"Per installarlo una volta sola e accorciare il comando:",[127,487,489],{"className":129,"code":488,"language":131,"meta":132,"style":132},"npm i -g @enricodeleo/crudio\ncrudio ./openapi.yaml\n",[134,490,491,505],{"__ignoreMap":132},[137,492,493,496,499,502],{"class":139,"line":140},[137,494,495],{"class":150},"npm",[137,497,498],{"class":154}," i",[137,500,501],{"class":184}," -g",[137,503,504],{"class":154}," @enricodeleo/crudio\n",[137,506,507,510],{"class":139,"line":147},[137,508,509],{"class":150},"crudio",[137,511,158],{"class":154},[11,513,514,515,482],{},"Se lo provi e ti torna utile — o se hai un'idea su dove dovrebbe fermarsi il confine tra \"mock\" e \"backend vero\" — scrivimene: ne parliamo nei commenti oppure ",[476,516,518],{"href":517},"mailto:hello@enricodeleo.com","contattami",[520,521,522],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":132,"searchDepth":147,"depth":147,"links":524},[525,526,527,528,529],{"id":40,"depth":147,"text":41},{"id":87,"depth":147,"text":88},{"id":223,"depth":147,"text":224},{"id":440,"depth":147,"text":441},{"id":466,"depth":147,"text":467},[531],"dev",null,"2026-06-30T09:00:00.000Z","Ho costruito Crudio, uno strumento open source che trasforma una spec OpenAPI 3.x in un backend mock vero: stateful, con validazione sullo schema e persistenza su disco. Tutto derivato dal tuo contratto.","md",{},"/crudio-da-una-spec-openapi-a-un-backend-mock-stateful",{"title":6,"description":534},"crudio-da-una-spec-openapi-a-un-backend-mock-stateful",[541,542,543,544,545,546,547,548,549],"openapi","api","mock","javascript","nodejs","express","testing","frontend","opensource","41PVj89zXppGTYqqZy-gKG579Ew_d6Q7vo873Z0xwrw",1782776288813]