{"templateId":"markdown","versions":[{"version":"v1","label":"1.x (archive)","link":"/docs/cli/v1/guides/migrate-to-generated-client","default":false,"active":false,"folderId":"6f4800fc"},{"version":"v2","label":"2.x (current)","link":"/docs/cli/guides/migrate-to-generated-client","default":true,"active":true,"folderId":"6f4800fc"}],"sharedDataIds":{"sidebar":"sidebar-docs-cli.sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"redocly_category":"Redocly CLI","type":"markdown"},"seo":{"title":"Move an existing app to a generated client","description":"OpenAPI-generated documentation tool with 24k+ stars on Github - make APIs your company's superpower.","siteUrl":"https://redocly.com","image":"/assets/redocly-card.f670aae34a39545a5ea633a540cb3a4a333a1f23bb2ed3c4a1b17a5fbcf0ac85.db81178d.png","lang":"en-US"},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"move-an-existing-app-to-a-generated-client","__idx":0},"children":["Move an existing app to a generated client"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"introduction","__idx":1},"children":["Introduction"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Most applications already talk to their API through code somebody wrote by hand: a types file, a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]}," wrapper, and a set of helpers around them."," ","This guide tells you how to replace that code with a generated client, one API at a time, without a rewrite."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It assumes you have an OpenAPI description of the API."," ","If the description is out of date, read ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#expect-the-description-to-be-wrong"},"children":["Expect the description to be wrong"]}," first, because that step decides how the rest of the work feels."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"generate-beside-your-current-client","__idx":2},"children":["Generate beside your current client"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Generate into a new path and change nothing else:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"redocly generate-client openapi.yaml --output src/api/generated/client.ts\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your application still runs on the old code."," ","You now have both, so you can compare them and migrate one call site at a time."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Put the command in your build so the client cannot drift from the description:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"scripts\": {\n    \"generate\": \"redocly generate-client openapi.yaml -o src/api/generated/client.ts\",\n    \"build\": \"npm run generate && tsc\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Commit the generated file."," ","A reviewer then sees what changed in the API when you regenerate, and the build does not depend on the description being reachable."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"map-your-old-client-onto-the-new-one","__idx":3},"children":["Map your old client onto the new one"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The pieces of a hand-written client have direct equivalents:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What you have now"},"children":["What you have now"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What replaces it"},"children":["What replaces it"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A types file, generated or hand-written"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The types in the generated client. Every operation carries its own request and response types."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]}," wrapper with a base URL"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["configure({ serverUrl })"]},", or the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["servers"]}," entry of the description."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Auth headers added by hand"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["configure({ auth: … })"]},", or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client.auth.bearer(…)"]}," on one instance. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#authentication"},"children":["Authentication"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A retry helper"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["configure({ retry: { retries: 3 } })"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#retries"},"children":["Retries"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A hand-rolled pagination loop"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Declared ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#pagination"},"children":["pagination"]}," with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".pages()"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".items()"]}," iterators."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Interceptors for logs, traces, or headers"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#middleware"},"children":["Middleware"]},", which sees each operation's id and tags as literal types."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["An existing configured request library"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["configure({ fetch })"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#the-http-layer"},"children":["The HTTP layer"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Response shapes checked by hand"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#runtime-validation"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["zod"]}," generator"]}," and its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["zodValidation()"]}," middleware."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Hand-written API mocks in tests"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#generators"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mock"]}," generator"]},": MSW handlers and typed data factories."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Two of those replace whole files rather than lines."," ","Pagination loops and mock fixtures are usually the largest deletions in a migration of this kind."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"migrate-the-call-sites","__idx":4},"children":["Migrate the call sites"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Work per module, not per operation."," ","For each module, change the imports to the generated client and let the compiler list what breaks:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"// Before\nimport { getOrder } from '../api/orders';\nconst order = await getOrder(orderId);\n\n// After\nimport { getOrderById } from '../api/generated/client.js';\nconst order = await getOrderById({ path: { orderId } });\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Three differences account for most of the compiler errors:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Operation names come from the description."]}," The generated name is the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operationId"]},", so ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["getOrder"]}," becomes whatever the description calls it."," ","If the names read badly, fix them in the description: every consumer improves at once."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Inputs are grouped by layer."]}," Path parameters go in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["path"]},", query parameters in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["query"]},", the body in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["body"]},", and headers in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["headers"]},"."," ","A call that passes an undeclared key fails with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TypeError"]}," that names the key, so a wrong call cannot reach the network."," ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/commands/generate-client#options"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--args-style flat"]}]}," merges the layers into one object instead, which some hand-written wrappers are closer to."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Errors are typed."]}," By default an operation throws ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ApiError"]}," on a non-2xx response."," ","With ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client#error-handling"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--error-mode result"]}]}," it returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ data, error, response }"]}," instead, which is closer to some hand-written wrappers."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"expect-the-description-to-be-wrong","__idx":5},"children":["Expect the description to be wrong"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A generated client holds your code to the description, so the first run tells you where the two disagree."," ","This is the useful part of the migration, and it is also the part that surprises people, so plan for it."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Turn on runtime validation early:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"import { use } from './api/generated/client.ts';\nimport { zodValidation } from './api/generated/client.zod.ts';\n\nuse(zodValidation()); // invalid requests throw; response drift warns\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Requests that do not match the description throw before the network call, and responses that do not match warn by default."," ","Both point at the field and the operation."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When a check fails, fix the cause rather than the check."," ","A failure is either a defect in your code or a defect in the description, and disabling validation keeps both."," ","Correct the description, regenerate, and every consumer of that API gets the correction."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"migrate-the-tests-too","__idx":6},"children":["Migrate the tests too"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A generated client that every test mocks away is a generated client that no test exercises."," ","The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mock"]}," generator emits MSW handlers and typed factories, so a test can run the real client against a fake network:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"import { listOrdersHandler, createOrder } from './api/generated/client.mocks.ts';\n\nserver.use(listOrdersHandler({ orders: [createOrder({ id: 'ord_1' })] }));\n// the code under test now issues a real request through the real client\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This moves argument building, URL construction, and response parsing into the test, which is where the migration's remaining defects usually hide."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"delete-the-old-client","__idx":7},"children":["Delete the old client"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Remove the old module when its last call site is gone, and keep the deletion in its own commit."," ","The generated client replaces code rather than adding a layer, so the net line count of a migration is usually negative."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"resources","__idx":8},"children":["Resources"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/commands/generate-client"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["generate-client"]}," command"]},": the flags and the invocation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/use-generated-client"},"children":["Use the generated client"]},": auth, retries, middleware, pagination, and the add-on generators."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/guides/customize-client-generation"},"children":["Customize client generation"]},": publisher defaults, custom generators, and ejecting a built-in generator."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/cli/configuration/reference/client"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client"]}," configuration"]},": the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["redocly.yaml"]}," block."]}]}]},"headings":[{"value":"Move an existing app to a generated client","id":"move-an-existing-app-to-a-generated-client","depth":1},{"value":"Introduction","id":"introduction","depth":2},{"value":"Generate beside your current client","id":"generate-beside-your-current-client","depth":2},{"value":"Map your old client onto the new one","id":"map-your-old-client-onto-the-new-one","depth":2},{"value":"Migrate the call sites","id":"migrate-the-call-sites","depth":2},{"value":"Expect the description to be wrong","id":"expect-the-description-to-be-wrong","depth":2},{"value":"Migrate the tests too","id":"migrate-the-tests-too","depth":2},{"value":"Delete the old client","id":"delete-the-old-client","depth":2},{"value":"Resources","id":"resources","depth":2}],"frontmatter":{"seo":{"title":"Move an existing app to a generated client"}},"lastModified":"2026-08-25T15:47:38.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/cli/guides/migrate-to-generated-client","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}