{"_id":"@iremlopsum/apify","_rev":"18-3d06f55326053e21c4f7666234d04cd1","name":"@iremlopsum/apify","dist-tags":{"latest":"4.4.2"},"versions":{"1.0.0":{"name":"@iremlopsum/apify","version":"1.0.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@1.0.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"b03423b3b2dd1526506ef6546f57f5765bc3bd27","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-1.0.0.tgz","fileCount":43,"integrity":"sha512-RPXz3libJHxZZysVDyMagqHFwFRAxq1NlhL2vIjUjTNSrM/GwNeeORM5ZGwMjh7L+bW/HRveaVsvYJnTd1ESow==","signatures":[{"sig":"MEUCIEpE3gDk4LkwzN20xVWTemWwTvMHtosaBCY4HafBCDMaAiEA1Jy0CfXnGP7Ax62LCMcOYjhVfPZq9TV9KXbB+2N7w9s=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":177370},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"e996cf3b1efad300ebb95f41ceeb050b829165d4","scripts":{"test":"vitest","build":"tsc","test:run":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run test:run && npm run build"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.11.0","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"_nodeVersion":"25.8.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0"},"_npmOperationalInternal":{"tmp":"tmp/apify_1.0.0_1775070344177_0.7348771905426563","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@iremlopsum/apify","version":"2.0.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@2.0.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"7126e16c4eb237b584d0e3a18d2291c18ea81e35","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-2.0.0.tgz","fileCount":55,"integrity":"sha512-4Tl/smKUXtVJSicSPMwNzkHXuGpDPaNAg/OqZIWBVMUsI826k2kGuMf9AABnXlrFkulhneIuPo/kGrP0Z4M+YQ==","signatures":[{"sig":"MEUCIQD1rCLB5jeGDWQ/hpAwaKS3jCwSgex4t0lgkxBzK2F+tAIgTbF1TM3C+CmEIGjJS6MPz7b4tcRS6dik/J1ADfnrFdM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222418},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"03e2cab215c412d28123b5abb3c933b676e829f1","scripts":{"test":"vitest","build":"tsc","test:run":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run test:run && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.11.0","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"_nodeVersion":"25.8.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_2.0.0_1778286053251_0.4880872122213502","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@iremlopsum/apify","version":"2.1.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@2.1.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"b500fe90486c3c7bc645edce9c66ce60302a7f3a","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-2.1.0.tgz","fileCount":29,"integrity":"sha512-m4nO1HcyGAkfrlBCT7uzYkOMqXzocS6y6wmPN4Y4SoPtWrTgAR2NCS7Tmz5BwYj2nrUFcrgyE/sdSvnm3h6r2A==","signatures":[{"sig":"MEUCIQCx7vy5/Wp+Q/m6BNIilqe62BxYXDVNJcvzruL6HT3vQgIgLMnmHbmithg8V5sQiebL3ipZ0Dv1zvxRGcR1Ci2utIM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":112740},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"fcddfa02485abe518752ce5fb4873a8bdcd0200a","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_2.1.0_1789314107824_0.46199818257779635","host":"s3://npm-registry-packages-npm-production"}},"2.2.0":{"name":"@iremlopsum/apify","version":"2.2.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@2.2.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"a5b0dbef40911fde5a5d9bd98f5958ea4220d009","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-2.2.0.tgz","fileCount":43,"integrity":"sha512-OtjfrI/GIgQ/k+aQ54JRNYfPICwq8jt4SeIu9gU3KbGud7s8Wpz0ZuEjuc3N4kfxpZdh2aPAMdHJEe1r4h+1ow==","signatures":[{"sig":"MEUCIBB5pJzWllAYtMO4SGOoZ6c9d+SpBlD/CMvg2FNcREKiAiEA4oM3tbeJ13GdTa8QFUBieMCh2cBej9zBIiMILlEWzoM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":185060},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"7713985e870051b1bce86d4fd43fe5a6aec281c2","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_2.2.0_1789405973329_0.27249404604221916","host":"s3://npm-registry-packages-npm-production"}},"2.2.1":{"name":"@iremlopsum/apify","version":"2.2.1","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@2.2.1","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"fc00b8e837791ca81bb3d03d8b48ae17789487ef","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-2.2.1.tgz","fileCount":43,"integrity":"sha512-pM1anzIRcXjVB4/gdFQ1epmmvpAOVWXaQE1d9NH/yN/6nb9OaymxdjZp1sxvNWlcgJR4fcZMd3Uiq4Cu3ayexQ==","signatures":[{"sig":"MEYCIQCwXsbDSzyErLK7mkrIrSEq6y4FqibX3mzLP50V88tSGAIhAJ/4S7fYYvJnRfQsRpkPfRu01MOGIZlB7XeOXSY8mMgu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDwhwNjKX0n7AVU3U1RJLiG/nnvZBVe1JnpqOK+5314PAIhAI3tjzim/+DCE30qkWICmrShRnPb+9HY3+IouHqJZjes","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":194177},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"ec9d0c4d83007c1603c77c73e171e221a4a18bfd","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_2.2.1_1789457392666_0.8838877365971005","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"name":"@iremlopsum/apify","version":"3.0.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@3.0.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"c52981e8ae6f94d62915cf1300436b128b26a7e8","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-3.0.0.tgz","fileCount":45,"integrity":"sha512-oWl+4bGAfT4a7SMcPQDXujLasruGKtzFDunmboip+ymEGl8fslMFdN2hQLGfM3cafVkhQgidiLdEYjssUycLig==","signatures":[{"sig":"MEYCIQD51LupLz8cGZdPtAcNisD04j8hxOebn9ghQhhckwf8UwIhAKN8jqt/1oYkg7fLymJiiJgOvJ/aLKZjOXktHQYIk4lB","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCUYnh2U8I8FiBoh7m1QKMsrRuH0JMOi7C/DTqwXBYluwIhAJSO8kI9OcjB+VXkBqMhGAVMoE3egx6HuCCiBKOPZ3NH","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244707},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"222bc1b794fc6ffed2ad92a999afc0e16b887329","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_3.0.0_1789543520487_0.13875331964736914","host":"s3://npm-registry-packages-npm-production"}},"3.1.0":{"name":"@iremlopsum/apify","version":"3.1.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@3.1.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"4081e7babe9c8d4e5a24f4e9baedb16ab95ce440","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-3.1.0.tgz","fileCount":45,"integrity":"sha512-xrnBZzilUb60pahkrTyCMOVfWa2VMXudNzel9z8g93BRaKK2gEW6R/8RPnhyJgcxsH5fulkcXtFS3ZmI9jULCQ==","signatures":[{"sig":"MEQCIGoR/Y+NEqhH4Sl/i/QyraPtlyer79JHsVrcZdQQLIxWAiBjIk7gUj3+a/++u/zSr27EMri7WDduhXNhNkuM2PcmOQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCrT0m7OXk3Wdz6Dd++I4JtS0P5CDsBxJwfmaeNWBFAswIgeZGvoviScaqCbpm4f32Q/grjBZv8P73Qf+sd8jWCob8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":255316},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"26461f880a28dbf6bbbf21c8fd5f5577aa640c07","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_3.1.0_1789552613007_0.6607560250795508","host":"s3://npm-registry-packages-npm-production"}},"4.0.0":{"name":"@iremlopsum/apify","version":"4.0.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.0.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"129b9e413baae92904ee242d1bb95a9de51ad98f","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.0.0.tgz","fileCount":45,"integrity":"sha512-iHldYZfPMCVYyBtmxYtEPuFYcNjH+m0QR/zs7P/7mRhwNWQrLjusZorNxLTtJIfypRNzO7eNEVSv64fiZDEkDg==","signatures":[{"sig":"MEQCIH1QtEBHz8RPFSe/IlK3cmaTv9cfSoeH8Y5kKb6wLyI6AiAjpkuU5Rkqet+7ymG7XzmmQ17e7AzT90WXPYgDj3KnBQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCakuOj42yXh/fi3Kuk6aVBT69brnzOAbBhtHPzqL/gNwIgJJs1My9eS3ihDHDS8oDJoq32qgHqHcN6RKWO447oN0w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":262707},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"696db28f000958d2a6e3ac42f955ae4076ec5a7a","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.0.0_1789559145705_0.17203685332993168","host":"s3://npm-registry-packages-npm-production"}},"4.0.1":{"name":"@iremlopsum/apify","version":"4.0.1","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.0.1","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"4485817c4b511dbe2d4bef81fa0d722252416ed7","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.0.1.tgz","fileCount":45,"integrity":"sha512-76euPAHTXRmNLQuBIZVnMec/4k/yNW/lpFc0DPhSQpyBwRPjycjLum4oQSQx9U0Y5wIoZ/sNueuRUvGxOnR/vw==","signatures":[{"sig":"MEYCIQCxjTVqiF94t0UOd3Ul4A2DFm7IMHA39KuQR/pNspwZEgIhALIWvt08C9nWrA4HgBcfx8wh1Oe6kCD0TGcK41xr5sl1","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIC5gzVfps+xE1Q8dKm8Zts+S6ghLv/Qo40L1x7EC7jnhAiBhIWLIf1/E+BMVXB9+E0dxv5lWLQCC7Rzecz3zFhIn8w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":264872},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"18a3b3a3a5802f841d69c3732167c04e344687a0","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.0.1_1789564879382_0.14486720089532112","host":"s3://npm-registry-packages-npm-production"}},"4.0.2":{"name":"@iremlopsum/apify","version":"4.0.2","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.0.2","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"417dd5435664626b9df3d8329090c3de0d012478","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.0.2.tgz","fileCount":45,"integrity":"sha512-6xWBIRRiaW6s5O+WOeAEtgYKWn0bLpU2/zpsxnnNE9m/aA17RMjy4UVpu9cbvlHT4ZHy5+UeTP9544KiuVC8Yg==","signatures":[{"sig":"MEYCIQC945JMkPrgj1VoR5RZR6OW1uehHcR0kY78/Jv6IB5+ZAIhAJ/GppIaDeIlbA9nqCZ27oJKsmcFCObo/hwJil+XKD0F","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDdfLKBvpN/jBdYeKu+vBq+LkG3oRHxZNwf9BCAXnRIQwIhAN6tbg+3/GJxXGrF3+xhL/x/M6TRDf+/vKaUFORTCyrV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":266698},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"852f5dc612141b8b5a1ac099263490b3acd3ecf8","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.0.2_1789677703160_0.04641731062183152","host":"s3://npm-registry-packages-npm-production"}},"4.1.0":{"name":"@iremlopsum/apify","version":"4.1.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.1.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"0be2573df9317dfa8afaec21bfd87a421834ed8c","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.1.0.tgz","fileCount":47,"integrity":"sha512-CeZlNKSRbc/3LgVrFRQy7imzKixp1VXAfwyyxp/+3OlRps9OBWtRTJ8Ox3PFsHfb2K0miUZfsEEWl29LgFezuQ==","signatures":[{"sig":"MEUCIAMLdEttZZ6hCqV/dtIRiKs5k5rBcUtEtcslcSQRJDsiAiEAu8S5L65pcTeb2GMV7XkLZ8ZEe9xZEGwsIoldfPjv09Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCNSu1eDNZ6r08CQyBbEovbK/OrIiJ/5vzJEBqornKcHwIgcEQ4o3c1NGEcPVS2GBaIM/sIIX7bncdSyKAG8YvB1Gg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":277935},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"5f4b7b84af34fb24729d7c933e9c1c971bcedb21","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.1.0_1789751734011_0.6797275881894043","host":"s3://npm-registry-packages-npm-production"}},"4.1.1":{"name":"@iremlopsum/apify","version":"4.1.1","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.1.1","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"ee33506f8e3d29ad97a629986890dedc7bc6867f","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.1.1.tgz","fileCount":47,"integrity":"sha512-JBq7h1EIKLl+nGDhqvDYF4GDnT6USjQoIH6BtK7jXnbsYOgxI4TKX5JC8KTbQct/hxn8tI9U73PylAL0rZ/4cg==","signatures":[{"sig":"MEQCIByKqdOSeoEm5SE0as2MAeEvpFuORyeZMT8dsml7PJ/OAiAes+ELgxq96E+2YXprSSX6cnl+dEcUWp6X3i83FpjeGQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCJYHxNht6rjA82tt2H0KCmsE+KUokPECH6SVAmvV25swIgMub9dCppdqR1WT1Zxt8k1QZci6N06MTOlx142dslhqk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":279863},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"245e63284311f7abace5fb0f1a03faa64ca9d0e8","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.1.1_1789753596920_0.9960076758004519","host":"s3://npm-registry-packages-npm-production"}},"4.2.0":{"name":"@iremlopsum/apify","version":"4.2.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.2.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"b1323d7e1cac6b5306d70f79ec32c9bf080559de","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.2.0.tgz","fileCount":49,"integrity":"sha512-S2MpuS1LnWqkjO9akYEyTn2ADpaIWeiWEpP+ob2aV8y5SP2vmqS/yRKRM8mzsx28M13/oASk2HiAZqIoAfWJTw==","signatures":[{"sig":"MEUCIQCZt1A5UtT6IV+zdBRaxzGnk7jnLdplOPGLx6uFUmQqzQIgdm+eXl2kJ9TqWiwWoo3y1oXqBxwrHYb6AluoscVWTeo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCQlKi0ZQI/VnACEEm9bkF8pqr2xYHkDhn4qu9F1dcl8QIgbMPgp9N87FOn/LkwnCv8QPDB0ZjZYRwbo29OTkKslUw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":300208},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"3d97819103ab177cae8915f18788f8b47190ef27","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.2.0_1789769491946_0.8315976132000269","host":"s3://npm-registry-packages-npm-production"}},"4.2.1":{"name":"@iremlopsum/apify","version":"4.2.1","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.2.1","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"dd008001eb07e73a44f10a3308ff9fff6b729f28","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.2.1.tgz","fileCount":49,"integrity":"sha512-wmMu5nMoI7kP0b8bkAK81EZfYS8hRiu6RFOtgBNZWdPO4c7MkT3uQimK5j3gafcnNqJVcatCbc3Vzq/Zppxdzw==","signatures":[{"sig":"MEYCIQCPfJsATwJheluynDMD5oJXKjy01gAWITpL5XM0IbtROQIhALOPzlljvKq3G7tcsF+MbNbJ1saXrIjMrJz37yEAzgvi","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCJQWeH7T7i+WR8NOlNg5SGxLQ77ACJfTKG2eznjTjRhgIhAMcEGl6+amE/QczHWPQz7ziIcEZ3VQ+uPdJey2X/d5If","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":305021},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"98e6b9c10cc87914f30e35638ee50659d6f43326","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.2.1_1789827133131_0.9048073605501343","host":"s3://npm-registry-packages-npm-production"}},"4.3.0":{"name":"@iremlopsum/apify","version":"4.3.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.3.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"393a95f60b174b3ac7d5d5fd748089266518cbc7","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.3.0.tgz","fileCount":51,"integrity":"sha512-0mdEQIfPxZcItvJi6T6K3/a+XMyf9uyhaeEyjBXKbLzvaMGPyeKwnIlyFhnkSwTXSTTG8dPSOYIFVZcr6AjnwQ==","signatures":[{"sig":"MEQCIFWGdQ5WONfcZuAuJp70OOz+9X7vXaUWISUDO4wm5E4TAiBI7Rc65u1KgHCO+W1G3/v1pgW/aWX8JbWL40MJpthR8A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCuGjiwYgS5lWTYs8mh5msO9nYtk4g/Xf1q6jc9N5wRBwIhAJsQGe7Cis5M+KAqLZOTPtaretZwEmhVgadSpR8Gk5Qx","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":312675},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"42363ee125a8c5b28e4cd73f3b39c8bd6c587197","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.3.0_1789829440371_0.7674506155189686","host":"s3://npm-registry-packages-npm-production"}},"4.4.0":{"name":"@iremlopsum/apify","version":"4.4.0","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.4.0","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"e702ac226406163b623868c12bbb16359b7be295","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.4.0.tgz","fileCount":51,"integrity":"sha512-mHir+Ko04YaS1GnSkn17BtvLxuBp4U9aSfUQ5zdCX7Pnynvpgp16zznjFbgF8c59Y7dnCFOcgn1QulDuuuj8tA==","signatures":[{"sig":"MEQCIDIzpV/DsLrxKnFBAxbibXTgZgNkapM6TJTx9lWQCE+RAiAtf6ZVqVsZRxgzfwAv8kt024wBJjzz7Z3y8l3XueFNLQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCbise2czs6k1RxEfwemcheVLjJ5g8i8htiYGa3+1PYPgIgPd903I97BB4Y3DgVj74nHfMOQRkA5DLmblfMEhsbk90=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":320391},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"c5992a2904ff0478f44c3b8e503650db45907034","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.4.0_1789843276569_0.31445528979823156","host":"s3://npm-registry-packages-npm-production"}},"4.4.1":{"name":"@iremlopsum/apify","version":"4.4.1","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"author":{"name":"Kristjan Vool"},"license":"MIT","_id":"@iremlopsum/apify@4.4.1","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"homepage":"https://github.com/iremlopsum/apify#readme","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"3852250cabb21c18db85326d49dafa33a866047b","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.4.1.tgz","fileCount":51,"integrity":"sha512-wq5jvKNTlkE9ogZKPtpjgZczafmov+GT38a+t4Kl6EY++vtW4sZSF19b3ungEKKVBfR2JUz85n2qovymfpWrRw==","signatures":[{"sig":"MEUCIFm7qACb7mk1gL4U4xHyVJ2s3M0sInjeRhFAIOdc4rt4AiEA4MfzhRd4s1aBEi14O6pVAbZEWBWA0fQ0pNR2KyDQum8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDv61aLVqpIFfaFAasvaRZb6sP/LqLhBGFp55/lGS1TJAIgTb2i1AZ8M+TyAghvN/L6CCVQRhUeTZKzHXh0nqSCvAI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":323444},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"8a15cdf38e0fabc787530f8d8aa2db452ba9d5c3","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"tmp":"tmp/apify_4.4.1_1790160490967_0.15778444903429278","host":"s3://npm-registry-packages-npm-production"}},"4.4.2":{"_id":"@iremlopsum/apify@4.4.2","bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"dist":{"shasum":"632de1bda530de40a22be4358047d46bec7cfc51","tarball":"https://registry.npmjs.org/@iremlopsum/apify/-/apify-4.4.2.tgz","fileCount":53,"integrity":"sha512-fyOGqWqU/r+ly5inEuGv6DeUr5c1c1pb6r9lMPFYHIB3MgbtoBM+3KVBRokQ8+5nPqHsLZhbMx0IJ3miqjey3g==","signatures":[{"sig":"MEQCICPdn4ZOkoKCQEnGC80TlNfXgZ5d9Oc6RdMkilNeVUxSAiAvnYgMd2avi2oecHKDcqqO0uH89tHiGGo0hvJQhtijUg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIH4bDaSS3G1HToRAg7v95VQ88twXbCts9IhXg+h8euETAiBqWPrmxzBTTM5sX8SKV56adGd5O6ciOMqYdNOBAqnLng=="}],"unpackedSize":341738},"main":"./dist/index.js","name":"@iremlopsum/apify","type":"module","types":"./dist/index.d.ts","author":{"name":"Kristjan Vool"},"engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"},"./middleware":{"types":"./dist/built-in-middleware.d.ts","import":"./dist/built-in-middleware.js"}},"gitHead":"bf3719aaa743d1b0e099ef8d9d60e24ad8e19170","license":"MIT","scripts":{"test":"vitest","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.types.json","clean":"node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"","test:run":"vitest run","typecheck":"tsc --noEmit","test:types":"vitest run --typecheck","prepublishOnly":"npm run typecheck && npm run test:types && npm run test:run && npm run test:integration && npm run build","test:integration":"vitest run --config vitest.integration.ts"},"version":"4.4.2","_npmUser":{"name":"kristjan_vool","email":"kristjanfou@gmail.com"},"homepage":"https://github.com/iremlopsum/apify#readme","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"_npmVersion":"11.12.1","description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","directories":{},"maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"sideEffects":false,"_nodeVersion":"26.0.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","typescript":"~5.8.0","@types/node":"^22.19.18"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/apify_4.4.2_1790163592942_0.9381560565592058"}}},"time":{"created":"2026-04-01T19:05:44.054Z","modified":"2026-09-23T11:39:53.272Z","1.0.0":"2026-04-01T19:05:44.321Z","2.0.0":"2026-05-09T00:20:53.433Z","2.1.0":"2026-09-13T15:41:47.958Z","2.2.0":"2026-09-14T17:12:53.476Z","2.2.1":"2026-09-15T07:29:52.773Z","3.0.0":"2026-09-16T07:25:20.572Z","3.1.0":"2026-09-16T09:56:53.100Z","4.0.0":"2026-09-16T11:45:45.789Z","4.0.1":"2026-09-16T13:21:19.487Z","4.0.2":"2026-09-17T20:41:43.258Z","4.1.0":"2026-09-18T17:15:34.226Z","4.1.1":"2026-09-18T17:46:37.005Z","4.2.0":"2026-09-18T22:11:32.061Z","4.2.1":"2026-09-19T14:12:13.231Z","4.3.0":"2026-09-19T14:50:40.464Z","4.4.0":"2026-09-19T18:41:16.676Z","4.4.1":"2026-09-23T10:48:11.072Z","4.4.2":"2026-09-23T11:39:53.081Z"},"bugs":{"url":"https://github.com/iremlopsum/apify/issues"},"author":{"name":"Kristjan Vool"},"license":"MIT","homepage":"https://github.com/iremlopsum/apify#readme","keywords":["fetch","api","client","http","typescript","type-safe","middleware","rest"],"repository":{"url":"git+https://github.com/iremlopsum/apify.git","type":"git"},"description":"Runtime-agnostic, type-safe API client built on standard fetch. Zero dependencies.","maintainers":[{"name":"kristjan_vool","email":"kristjanfou@gmail.com"}],"readme":"# apify\n\nRuntime-agnostic, type-safe HTTP client for REST and GraphQL. Built on standard `fetch`. Zero dependencies.\n\n- **Unified API** — REST and GraphQL share the same `Result<T>` shape, middleware stack, and error contract\n- **Never throws** — every call returns `{ data, error, response, retry }`, no try/catch required\n- **Composable middleware** — retry, cache, dedupe, auth, logging — applied at global, per-endpoint, or per-call level\n- **Types by inference** — declare params and response once on the endpoint definition; types flow to every call site automatically\n- **Runtime-agnostic** — Node.js 20+, browsers, Bun, Deno, Cloudflare Workers, React Native — any environment with `fetch`\n- **Tiny** — about **2.9 kB gzipped** for a REST-only import, 4.4 kB for everything including GraphQL and all middleware; tree-shaking drops what you do not import\n\n```\nnpm install @iremlopsum/apify\n```\n\n## Table of Contents\n\n- [Getting Started](#getting-started)\n- [REST API](#rest-api)\n  - [Request](#request)\n  - [`defineRequest`](#definerequest)\n  - [Response validation](#response-validation)\n  - [Pagination](#pagination)\n  - [Query strings](#query-strings)\n  - [Result](#result)\n  - [Error handling with `onError`](#error-handling-with-onerror)\n  - [Middleware](#middleware)\n    - [Built-in middleware (retry, cache, log)](#built-in-middleware)\n  - [Content types](#content-types)\n  - [Response parsing](#response-parsing)\n  - [Cancellation](#cancellation)\n  - [Timeout](#timeout)\n  - [Sharing](#sharing)\n  - [TypeScript](#typescript)\n- [GraphQL Client](#graphql-client)\n  - [Queries and mutations](#queries-and-mutations)\n  - [GraphQL errors](#graphql-errors)\n  - [Middleware](#middleware-1)\n- [Testing](#testing)\n- [Philosophy](#philosophy)\n- [API Reference](#api-reference)\n\n## Getting Started\n\nDefine your endpoints as `Request` instances, wire them into a client with `createApi`, and call them with full type safety.\n\n```ts\nimport { createApi, Request } from '@iremlopsum/apify'\n\n// 1. Define your endpoints\ninterface User {\n  id: string\n  name: string\n  email: string\n}\n\nconst getUser = new Request<{ id: string }, User>({\n  method: 'GET',\n  path: '/users/:id'\n})\n\nconst createUser = new Request<{ name: string; email: string }, User>({\n  method: 'POST',\n  path: '/users'\n})\n\n// 2. Create the client\nconst api = createApi({\n  baseUrl: 'https://api.example.com',\n  requests: { getUser, createUser },\n  onError: (error) => console.error(`${error.request.method} ${error.request.url}`, error.status)\n})\n\n// 3. Make a call — params and response are fully typed\nconst { data, error, retry } = await api.getUser({ id: '42' })\n\nif (error) {\n  console.error(error.status, error.body)\n  return\n}\n\n// data is typed as User\nconsole.log(data.name)\n```\n\n## REST API\n\n### Request\n\nEach API endpoint is represented by a `Request` instance. The class is a typed config container -- it stores the recipe for how an endpoint should be called, but does not execute anything on its own.\n\n```ts\nimport { Request } from '@iremlopsum/apify'\n\nconst listItems = new Request<{ page: number; limit: number }, Item[]>({\n  method: 'GET',\n  path: '/items'\n})\n```\n\nThe two type parameters drive the entire type system:\n\n- `TParams` -- the shape of the params object the caller must provide (path params, query params, and body params combined).\n- `TResponse` -- the shape of the successful response data. This becomes the type of `result.data`.\n\nWhen an endpoint takes no params, use `Record<string, never>` and the generated method will accept an optional (or omitted) params argument:\n\n```ts\nconst health = new Request<Record<string, never>, { status: string }>({\n  method: 'GET',\n  path: '/health'\n})\n\n// Both work:\nawait api.health()\nawait api.health({})\n```\n\n#### Path parameters\n\nUse `:param` syntax in the path. Matching keys from the params object are substituted into the URL and excluded from the query string or body:\n\n```ts\nconst getItem = new Request<{ orgId: string; id: string }, Item>({\n  method: 'GET',\n  path: '/orgs/:orgId/items/:id'\n})\n\n// Calls GET /orgs/acme/items/42\nawait api.getItem({ orgId: 'acme', id: '42' })\n```\n\n#### `responseType`\n\nControls how the response body is parsed. Defaults to `'json'`.\n\n```ts\nconst downloadFile = new Request<{ id: string }, Blob>({\n  method: 'GET',\n  path: '/files/:id',\n  responseType: 'blob'\n})\n```\n\nSee [Response parsing](#response-parsing) for all options.\n\n#### `dedupe`\n\nWhen `true`, firing a new call to this endpoint auto-cancels any previous in-flight call. Useful for search-as-you-type or rapidly changing filters:\n\n```ts\nconst searchUsers = new Request<{ q: string }, User[]>({\n  method: 'GET',\n  path: '/users/search',\n  dedupe: true\n})\n\n// If a second call starts before the first finishes, the first is aborted\nawait api.searchUsers({ q: 'hel' })\nawait api.searchUsers({ q: 'hello' }) // previous call is auto-cancelled\n```\n\n#### `share`\n\nWhen `true`, identical concurrent calls to this endpoint join a single in-flight request instead of firing their own. See [Sharing](#sharing) for the full contract — including its mutual exclusion with `dedupe` and what disables it.\n\n```ts\nconst getProduct = new Request<{ id: string }, Product>({\n  method: 'GET',\n  path: '/products/:id',\n  share: true\n})\n\n// Both calls join the same network request\nawait Promise.all([\n  api.getProduct({ id: '42' }),\n  api.getProduct({ id: '42' })\n])\n```\n\n#### `bodyAs`\n\nOverrides the default body serialization strategy. By default, GET/DELETE serialize params as query strings and POST/PUT/PATCH serialize params as a JSON body. Use `bodyAs` to invert that:\n\n```ts\n// DELETE endpoint that expects a JSON body\nconst bulkDelete = new Request<{ ids: string[] }, { deleted: number }>({\n  method: 'DELETE',\n  path: '/items',\n  bodyAs: 'body'\n})\n\n// POST endpoint that sends params as query string\nconst triggerJob = new Request<{ priority: number }, Job>({\n  method: 'POST',\n  path: '/jobs/trigger',\n  bodyAs: 'query'\n})\n```\n\n### `defineRequest`\n\n`new Request<TParams, TResponse>` makes you restate what the path already says,\nand nothing checks the two against each other:\n\n```ts\n// The params are restated by hand, and nothing checks them against the path:\nconst getUser = new Request<{ userId: string }, User>({ method: 'GET', path: '/users/:id' })\n\napi.getUser({ userId: '42' })  // compiles — then fails at runtime: buildUrl finds\n                               // no `:userId` to substitute, `:id` survives, and\n                               // the unresolved-token check throws\n```\n\n`defineRequest` infers the params from the path literal instead:\n\n```ts\nimport { defineRequest } from '@iremlopsum/apify'\n\nconst getUser = defineRequest<User>()({ method: 'GET', path: '/users/:id' })\n\napi.getUser({ id: '42' })      // ✓\napi.getUser({ id: 42 })        // ✓ — numbers are encoded\napi.getUser({ userId: '42' })  // ✗ Object literal may only specify known properties\n```\n\nParams the path does not name — query or body fields — go in the second type\nargument:\n\n```ts\nconst listRepos = defineRequest<Repo[], { page?: number }>()({\n  method: 'GET',\n  path: '/orgs/:org/repos',\n})\n\napi.listRepos({ org: 'acme' })            // ✓ page is optional\napi.listRepos({ org: 'acme', page: 2 })   // ✓\napi.listRepos({ page: 2 })                // ✗ org is required\n```\n\nIt also enforces the `responseType: 'none'` convention that `new Request` can\nonly document:\n\n```ts\ndefineRequest<undefined>()({ method: 'POST', path: '/ping', responseType: 'none' })  // ✓\ndefineRequest<User>()({ method: 'POST', path: '/ping', responseType: 'none' })       // ✗\n```\n\n**Why two calls.** TypeScript has no partial type-argument inference: if the response\ntype and the config were arguments to one call, supplying the response type explicitly\nwould stop the path from being inferred, and the checking would quietly do nothing.\nSplitting them keeps the response type explicit and the path inferred. Calling it\nwrong is a compile error, not a silent one.\n\n`new Request(...)` is unchanged and not deprecated — use it when the config is\nnot a literal, or when you do not want the path checked.\n\n### Response validation\n\nPass any [Standard Schema](https://standardschema.dev) validator — Zod, Valibot,\nArkType — and the response is checked before you see it. apify takes no\ndependency on one; Standard Schema is an interface, not a package.\n\n```ts\nimport { z } from 'zod'\n\nconst getUser = defineRequest()({\n  method: 'GET',\n  path: '/users/:id',\n  schema: z.object({ id: z.string(), name: z.string() }),\n})\n\nconst { data, error } = await api.getUser({ id: '42' })\n//      ^? { id: string; name: string } | null\n```\n\nThe schema supplies the response type, so there is no type argument to write —\nand no second place for it to drift out of date.\n\n**`data` is the schema's output.** A schema that transforms changes what you\nreceive:\n\n```ts\nconst getUser = defineRequest()({\n  method: 'GET',\n  path: '/users/:id',\n  schema: z.object({\n    id: z.string(),\n    createdAt: z.coerce.date(),        // the wire sends a string\n    role: z.string().default('user'),  // absent on the wire\n  }),\n})\n\nconst { data } = await api.getUser({ id: '42' })\ndata.createdAt   // a real Date\ndata.role        // 'user' when the server omitted it\n```\n\nThat is the point of validating through a schema rather than merely checking\none — but it does mean `data` is no longer byte-identical to the response.\n\nA response the schema refuses is an error `Result`, never a throw:\n\n```ts\nconst { error } = await api.getUser({ id: '42' })\nif (error?.kind === 'parse') {\n  console.error(error.body)  // the validator's issues\n  error.status               // the response's own status — the server was fine\n}\n```\n\nOnly the **success** body is validated. A non-2xx body is diagnostic and often a\ndifferent shape, so it is left alone.\n\nSchemas work on the GraphQL client too, validating the response's `data`:\n\n```ts\nconst me = new Operation<{}, User>({\n  operation: gql`query { me { id name } }`,\n  schema: UserSchema,\n})\n```\n\nThere the response type stays explicit — only `defineRequest` infers it.\n\n### Pagination\n\n`paginate` walks a paginated endpoint, yielding one `Result` per page:\n\n```ts\nimport { paginate } from '@iremlopsum/apify'\n\nfor await (const page of paginate(api.listItems, { limit: 50 }, {\n  next: (p, prev) => p.data.cursor ? { ...prev, cursor: p.data.cursor } : undefined,\n})) {\n  if (page.error) break\n  render(page.data.items)\n}\n```\n\n**`next` returns the next params, not a cursor.** That is what keeps this\nlibrary out of the business of guessing where a cursor goes — `cursor`?\n`page_token`? `after`? The previous params arrive as the second argument, so\nthe common case is a spread, and the same shape covers every scheme:\n\n```ts\n// offset\nnext: (p, prev) => p.data.items.length === prev.limit\n  ? { ...prev, offset: prev.offset + prev.limit }\n  : undefined\n\n// page number, driven by a Link header\nnext: (p, prev) => p.response.headers.get('link')?.includes('rel=\"next\"')\n  ? { ...prev, page: prev.page + 1 }\n  : undefined\n```\n\nReturn `undefined` or `null` to stop.\n\n**An error page is yielded, then the walk ends.** There is no data to read the\nnext cursor from, so there is nothing to continue with — and you see what\nfailed rather than a loop that quietly stopped.\n\n**`maxPages` is optional and has no default.** A ceiling exists if you want one;\nthe library will not invent a number, because a silent truncation at an\narbitrary limit looks exactly like reaching the last page.\n\n```ts\npaginate(api.listItems, { limit: 50 }, { next, maxPages: 100 })\n```\n\nAny other [`CallOptions`](#calloptions) — `signal`, `timeout`, `headers` — apply\nto every request, so one signal cancels the whole crawl.\n\n`paginate` yields pages, not items. Flattening would mean deciding which field\nholds the array, which is the convention-guessing `next` exists to avoid.\n\n### Query strings\n\nFor GET and DELETE requests (or any request with `bodyAs: 'query'`), params that are not consumed by path substitution are serialized as a query string using `URLSearchParams`.\n\nA `baseUrl` may carry its own query string — a fixed API key, say. Its params\nare merged ahead of the call's:\n\n```ts\nconst api = createApi({\n  baseUrl: 'https://api.example.com/v1?key=abc',\n  requests: { search: new Request<{ q: string }, Hit[]>({ method: 'GET', path: '/search' }) },\n})\n\nawait api.search({ q: 'hello' })\n// GET https://api.example.com/v1/search?key=abc&q=hello\n```\n\nMerging **accumulates**, it does not override: a call param whose key the base\nalready used produces both, `?key=abc&key=xyz`, and which one wins is the\nserver's decision. This differs from headers, where a per-call value replaces a\nglobal one — because array params already serialize as repeated keys\n(`tags=a&tags=b`), so collapsing duplicates would break them. If a base-level\nparam needs to vary per call, set it from middleware rather than the `baseUrl`.\n\n**A `#fragment` is refused.** A fragment is never sent to the server, so one in\na `path` or `baseUrl` cannot do what it appears to — and before 4.2.1 it\nsilently discarded the query string. It is now an error naming the offending\nvalue, rather than being stripped, so the dead code does not stay in your\ntemplate.\n\nSince 4.4.0 a fragment in a [`defineRequest`](#definerequest) `path` **literal**\nis also a compile error, so the endpoint is rejected where it is declared rather\nthan on every call:\n\n```ts\ndefineRequest<Doc>()({ method: 'GET', path: '/docs#section' })\n//                                          ^ Property '__fragmentInPath' is missing:\n//                                            a URL fragment is never sent to the server\n```\n\nThe check reads the literal, so a path assembled at runtime — or a\n`RequestConfig`-typed variable — still compiles and is caught by the runtime\nerror instead. `new Request` takes no path literal, so it has no equivalent\ncheck; this is one of the things `defineRequest` buys you.\n\n**A `#` inside a param *value* is not a fragment** and is never refused — it is\nescaped to `%23` and sent as ordinary data:\n\n```ts\nawait api.getDoc({ id: 'a#b' })   // → GET /docs/a%23b\nawait api.search({ tag: 'a#b' })  // → GET /search?tag=a%23b\n```\n\nOnly a `#` written into a `path` or `baseUrl` is refused, because that one was\nnever going to reach the server.\n\n| Input                          | Output                      |\n| ------------------------------ | --------------------------- |\n| `{ page: 1, limit: 20 }`      | `?page=1&limit=20`          |\n| `{ tags: ['a', 'b'] }`        | `?tags=a&tags=b`            |\n| `{ filter: null }`            | _(omitted)_                 |\n| `{ filter: undefined }`       | _(omitted)_                 |\n| `{ meta: { nested: true } }`  | **TypeError** (see below)   |\n\n**Arrays** use repeated keys (`tags=a&tags=b`), which is the most widely supported format across server frameworks.\n\n**`null` and `undefined`** values are silently omitted from the query string.\n\n**Nested objects** throw a `TypeError` with a descriptive message. Flatten the structure before passing. This is intentional -- there is no universal standard for serializing nested objects in query strings (brackets, dots, JSON), so the library refuses to guess.\n\n### Result\n\nEvery API call returns a `Result<TResponse>` instead of throwing. It's a discriminated union on `error`, not a plain interface:\n\n```ts\ninterface SuccessResult<TResponse> {\n  data: TResponse                          // parsed response\n  error: null\n  response: Response                       // always present on success\n  retry: () => Promise<Result<TResponse>>\n}\n\ninterface ErrorResult<TResponse> {\n  data: null\n  error: ApiError                          // structured error, see below\n  response: Response | null                // present for HTTP/parse failures, null for network/abort/timeout\n  retry: () => Promise<Result<TResponse>>\n}\n\ntype Result<TResponse> = SuccessResult<TResponse> | ErrorResult<TResponse>\n```\n\nCheck `error` first, then use `data` with confidence: `if (error) return` (or any other narrowing check on `error`) narrows `data` to `TResponse` for the rest of the function -- no `data!` assertion needed. That narrowing is only as accurate as `TResponse` itself, though: an endpoint that answers `204` or an empty `200` (a `DELETE`, most commonly) doesn't return a body at all -- declare it with `responseType: 'none'` and `TResponse` of `undefined`, rather than widening `TResponse` to `| null`, which since 4.0.0 does not work\nat all -- an empty body under `'json'` is a `'parse'` error -- see [Response parsing](#response-parsing) below. Branch on `error.kind` rather than `error.status` — `'network'`, `'abort'` and `'timeout'` all carry `status: 0`, but they call for different handling:\n\n```ts\nconst { data, error, response, retry } = await api.getUser({ id: '42' })\n\nif (error) {\n  switch (error.kind) {\n    case 'network':\n      // fetch itself failed -- user is probably offline\n      break\n    case 'timeout':\n      // the whole-operation deadline fired; report it\n      reportTimeout(error)\n      break\n    case 'abort':\n      // this call was cancelled (dedupe supersede, or your own signal) -- usually ignore it\n      break\n    case 'parse':\n      // a 2xx response arrived but its body didn't parse as `responseType`\n      console.error('unparseable response', error.status, error.body)\n      break\n    case 'middleware':\n      // a middleware threw -- a bug in your own pipeline, not a transient failure\n      console.error('middleware threw', error.body)\n      break\n    case 'http':\n      if (error.status === 401) redirectToLogin()\n      else console.error(error.status, error.body)\n      break\n  }\n  return\n}\n\n// error is null here, so `data` is narrowed to `User` -- no assertion needed\n// (this assumes getUser always answers with a body; an endpoint that\n// doesn't -- a DELETE returning 204, most commonly -- should use\n// responseType: 'none' instead, see the empty-body note above)\nconsole.log(data.name)\n```\n\n`response`'s body has already been consumed by the time you see it -- the library reads it to produce `data` (or `error.body`), so calling `response.json()` yourself throws \"Body has already been read\". Use `data`/`error.body`; `response` is for status, headers, and redirect metadata. (This applies only to results the library produces itself -- a `Response` you construct for `successResult()` in `testing.ts` still has a readable body.)\n\n#### `retry()`\n\nThe `retry` function re-executes the exact same request through the full middleware chain. Auth tokens are re-injected, logging fires again, everything runs fresh. This is useful for retry-after-refresh patterns:\n\n```ts\nconst { data, error, retry } = await api.getUser({ id: '42' })\n\nif (error?.status === 401) {\n  await refreshToken()\n  const retried = await retry()\n  // retried goes through the full middleware chain again\n}\n```\n\n#### `ApiError`\n\nThe error object on failed calls. It is not a subclass of `Error` -- it is a structured container for API-level error details.\n\n| Property     | Type      | Description                                                       |\n| ------------ | --------- | ----------------------------------------------------------------- |\n| `status`     | `number`  | HTTP status code (e.g., 404, 500). `0` for network errors, aborts, and timeouts. |\n| `kind`       | `'http' \\| 'network' \\| 'abort' \\| 'timeout' \\| 'parse' \\| 'middleware'` | What category of failure this is. See below. Required -- constructing an `ApiError` yourself (e.g. in custom middleware) must supply it. |\n| `statusText` | `string`  | HTTP status text (e.g., 'Not Found'). `''` for network errors.    |\n| `body`       | `unknown` | Parsed response body -- but for `'parse'`, one of: the thrown exception (a malformed body), the raw response text (an empty body, or a GraphQL response carrying no data), a schema's issues array (the response failed validation), or a value a schema threw. The native Error for network failures. |\n| `headers`    | `Headers` | Response headers. Empty `Headers` for network errors.             |\n| `request`    | `object`  | `{ method, url, params }` -- metadata about the failed request; `url` is the resolved, path-substituted address, falling back to the route template only when it could not be built. |\n| `partialData` | `unknown` (optional) | GraphQL data returned alongside `{ errors }` (partial success). Lives here, not on `Result.data`, so the `Result` stays a clean union: `data` is non-null iff `error` is null. `undefined` for every REST error and for GraphQL responses carrying no data. |\n\n`kind` exists because `status` alone cannot tell some outcomes apart: an HTTP error (`'http'`), a `fetch` failure with no response (`'network'`), a cancellation — your own signal, a dedupe supersede, or a whole-operation deadline firing — (`'abort'`/`'timeout'`), a 2xx (or non-2xx) body that failed to parse (`'parse'`), and a middleware that threw instead of the request itself failing (`'middleware'`) all need different handling, but `'network'`, `'abort'`, and `'timeout'` all carry `status: 0`.\n\n`'parse'` is for a **2xx** response that arrived but whose body failed to parse according to `responseType` -- you get the real `status`, a non-null `response`, and `kind: 'parse'`. A **non-2xx** response with an unparseable body is unaffected and still reports `kind: 'http'` -- the status code is checked before the body is parsed, so a 500 with a broken JSON body is still a 500, and `retryMiddleware`'s default 5xx retry still applies to it. An **empty** body under `'json'` is also `'parse'` -- see [Response\nparsing](#response-parsing). GraphQL applies the same rule to a 2xx response\ncarrying neither `data` nor `errors`. An optional [`schema`](#response-validation) on the request adds two more `'parse'` producers: a **2xx** body the schema refuses (`error.body` is its issues array) and a validator that throws (`error.body` is the thrown value) -- both only for the success body, never for a non-2xx one, which is never validated.\n\n`'middleware'` means a middleware threw rather than the request itself failing -- a bug in your own pipeline you'd fix, not a transient failure you'd retry. A middleware that propagates the library's own abort/timeout signal (verbatim, or wrapped one level as `.cause`) is classified `'abort'`/`'timeout'` instead, by provenance rather than by the reason's name -- see [Cancellation](#cancellation).\n\nYou can use `instanceof` to check if a value is an `ApiError`:\n\n```ts\nimport { ApiError } from '@iremlopsum/apify'\n\nif (error instanceof ApiError) {\n  // ...\n}\n```\n\n### Error handling with `onError`\n\nThe `onError` callback in `createApi` fires after the full middleware chain completes whenever the final result has an error. If a retry middleware recovers a 5xx to a 200, `onError` does not fire.\n\n```ts\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser, createUser },\n  onError: (error) => {\n    if (error.status === 401) redirectToLogin()\n    Sentry.captureException(error)\n  }\n})\n```\n\nThis fires for `kind: 'http'`, `'network'`, `'timeout'`, `'parse'`, and `'middleware'`. **It does not fire for `kind: 'abort'`** — a cancellation the library caused deliberately (your own `AbortSignal` firing, or a request superseded by `dedupe`) is not a failure worth reporting to an error tracker, unlike a `'timeout'`, which is a deadline you actually missed. The caller still gets the abort back in the `Result` either way; only the report to this callback is suppressed. It is a global hook for side effects (logging, telemetry, redirects) -- it does not change the result returned to the caller.\n\n### Middleware\n\nMiddleware follows the onion model (like Koa or Redux middleware). Each middleware wraps the next layer, can modify the request going in and the result coming out.\n\n```\nRequest → [Global MW → [Per-request MW → [Per-call MW → [fetch]]]]\n```\n\nA middleware function receives a `context` and a `next` function:\n\n```ts\nimport type { Middleware } from '@iremlopsum/apify'\n\nconst authMiddleware: Middleware = async (ctx, next) => {\n  // Before: modify the request\n  ctx.request.headers.set('Authorization', `Bearer ${getToken()}`)\n\n  // Call the next layer\n  const result = await next()\n\n  // After: inspect or transform the result\n  return result\n}\n```\n\n#### What middleware can do\n\n- **Modify the request** -- set headers, change the body, rewrite the URL.\n- **Short-circuit** -- return early without calling `next()` (e.g., serve from cache).\n- **Retry** -- call `next()` multiple times in a loop (e.g., retry on 5xx).\n- **Inspect the result** -- log, report errors, transform response data.\n\n#### Three layers\n\nMiddleware is applied at three levels. The execution order is global first, per-request second, per-call third:\n\n```ts\n// Global -- applies to every endpoint\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser, createUser },\n  middleware: [authMiddleware, logMiddleware]\n})\n\n// Per-request -- applies only to this endpoint\nconst getUser = new Request<{ id: string }, User>({\n  method: 'GET',\n  path: '/users/:id',\n  middleware: [cacheMiddleware]\n})\n\n// Per-call -- applies only to this single invocation\nawait api.getUser({ id: '42' }, {\n  middleware: [customTraceMiddleware]\n})\n```\n\n#### `skipMiddleware`\n\nRemove specific middleware for a single call by passing references to `skipMiddleware`:\n\n```ts\nconst retry = retryMiddleware(3)\n\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser },\n  middleware: [retry, logMiddleware]\n})\n\n// Skip retry for this one call\nawait api.getUser({ id: '42' }, {\n  skipMiddleware: [retry]\n})\n```\n\nComparison is by reference (`===`). Factory-style middleware like `retryMiddleware(3)` must be stored in a variable first -- calling the factory again creates a new reference that will not match.\n\n#### `MiddlewareContext`\n\nThe context object passed to each middleware:\n\n| Property              | Type      | Description                                              |\n| --------------------- | --------- | -------------------------------------------------------- |\n| `request.method`      | `string`  | HTTP method (GET, POST, etc.)                            |\n| `request.url`         | `string`  | Fully resolved URL with path params and query string     |\n| `request.path`        | `string`  | Original path template (e.g., '/users/:id')              |\n| `request.params`      | `unknown` | Original params object from the caller                   |\n| `request.headers`     | `Headers` | Merged headers -- middleware can add/remove entries       |\n| `request.body`        | `unknown` | Serialized body, or null for GET/DELETE                  |\n| `request.signal`      | `AbortSignal \\| undefined` | The signal handed to `fetch` -- replace it to impose your own cancellation policy |\n| `requestName`         | `string`  | Key name in the requests object (e.g., 'getUser')        |\n\n`request.signal` holds the call's own signal: the caller's `options.signal` merged with any `timeout` (and, under `share: true`, with the refcount that aborts the shared request once every sharer has given up). It is `undefined` only when there is none of those. The core fetch reads the field at call time, so replacing it takes effect -- that is all a timeout middleware needs:\n\n```ts\nconst timeout = (ms: number): Middleware => async (ctx, next) => {\n  ctx.request.signal = AbortSignal.timeout(ms)\n  return next()\n}\n\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser },\n  middleware: [timeout(5000)]\n})\n```\n\nUnder `dedupe: true` your signal is merged rather than discarded: the request is cancelled by whichever fires first -- your signal, or a newer call superseding this one. The dedupe signal is installed by the core fetch, so middleware reading `ctx.request.signal` before `next()` sees the caller's signal, not the dedupe one.\n\n**Pass `ctx.request.signal` on to any async work your middleware does itself** -- a token refresh, a lookup, a queue. The library will not wait for that work past the call's deadline or the caller's abort either way (see [Timeout](#timeout)), but a promise cannot be cancelled from outside: handing it the signal is the only thing that actually *stops* the work, instead of leaving it running in the background with its result discarded.\n\n#### Writing custom middleware\n\nA cache middleware that short-circuits on cache hits:\n\n```ts\nconst cacheMiddleware: Middleware = async (ctx, next) => {\n  const cached = cache.get(ctx.request.url)\n  if (cached) return cached\n\n  const result = await next()\n\n  if (result.data) {\n    cache.set(ctx.request.url, result)\n  }\n\n  return result\n}\n```\n\nAn error reporting middleware:\n\n```ts\nconst sentryMiddleware: Middleware = async (ctx, next) => {\n  const result = await next()\n\n  if (result.error && result.error.status >= 500) {\n    Sentry.captureMessage(`API error: ${ctx.request.method} ${ctx.request.url}`, {\n      extra: { status: result.error.status, body: result.error.body }\n    })\n  }\n\n  return result\n}\n```\n\n#### Built-in middleware\n\nThe library ships three optional middleware functions, importable from a separate entry point:\n\n```ts\nimport { retryMiddleware, logMiddleware, cacheMiddleware } from '@iremlopsum/apify/middleware'\n```\n\n**`retryMiddleware(options?: number | RetryOptions)`**\n\nAutomatically retries requests that fail, with a real backoff policy — exponential (or linear, or custom) delay curves, full jitter, `Retry-After` support, a configurable retry predicate, and an observational `onRetry` hook.\n\nThe numeric shorthand still works exactly as before — `retryMiddleware(2)` retries up to 2 additional times (3 total attempts) on a 5xx response:\n\n```ts\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getItems },\n  middleware: [retryMiddleware(2)]\n})\n```\n\nBy default, only server errors (`status >= 500`) are retried. Client errors (4xx), 429, and network errors (`status: 0`) are not — see the opt-in recipes below.\n\n**Retry policy**\n\nPass a `RetryOptions` object instead of a number for full control:\n\n```ts\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getItems },\n  middleware: [retryMiddleware({\n    max: 5,\n    delay: 'exponential',\n    baseDelay: 250,\n    maxDelay: 10_000,\n    jitter: true,\n    respectRetryAfter: true,\n    onRetry: ({ attempt, max, delay }) => console.log(`retry ${attempt}/${max} in ${delay}ms`),\n  })],\n})\n```\n\n| Option              | Type                                              | Default          | Description |\n| -------------------- | -------------------------------------------------- | ----------------- | ----------- |\n| `max`                | `number`                                            | `3`               | Additional attempts after the first. `retryMiddleware({ max: 2 })` means up to 3 total calls. |\n| `delay`              | `'exponential' \\| 'linear' \\| (attempt: number) => number` | `'exponential'`   | The delay curve. Exponential is `baseDelay * 2^(attempt-1)`; linear is `baseDelay * attempt`; a function receives the 1-based attempt number and returns milliseconds. |\n| `baseDelay`          | `number`                                            | `250`             | The first delay, in milliseconds, before jitter and `Retry-After` are applied. |\n| `maxDelay`           | `number`                                            | `30000`           | Hard cap applied to every computed delay, including a `Retry-After` value. |\n| `jitter`             | `boolean`                                           | `true`            | Full jitter: the actual delay is `Math.random() * computed`, per AWS's recommendation for de-synchronizing a thundering herd. Never applied to a `Retry-After` value — a server telling you exactly when to come back should not be randomized. |\n| `respectRetryAfter`  | `boolean`                                           | `true`            | Honor a `Retry-After` response header (delta-seconds or an HTTP-date) when present, replacing the computed delay outright (still capped by `maxDelay`). |\n| `retryOn`            | `(result: Result<unknown>, attempt: number) => boolean` | `r => (r.error?.status ?? 0) >= 500` | Whether to retry. Called with the 1-based *candidate* attempt number, even once `max` is reached, so a predicate that counts attempts sees one call per result. |\n| `onRetry`            | `(info: RetryInfo) => void`                         | —                 | Observational hook fired before each retry's delay elapses. Its return value is ignored, and a throw cannot fail the request — this is the only way to observe an in-progress retry sequence, since the call site sees nothing until the final result. |\n\n`RetryInfo` (the argument to `onRetry`): `{ attempt, max, delay, result }` — `attempt` is 1-based (the first retry is `1`), `delay` is the actual delay about to elapse (after jitter and `Retry-After`), and `result` is the `Result` that triggered this retry.\n\n**429 and network-error opt-in.** Both are deliberately excluded from the default `retryOn` — retrying a rate limit or a network failure by default would change behavior under existing callers on upgrade. Opt in explicitly:\n\n```ts\n// Retry 429 in addition to 5xx\nretryMiddleware({\n  retryOn: r => r.error?.status === 429 || (r.error?.status ?? 0) >= 500\n})\n\n// Retry network errors (status 0) too — but not aborts, which are also status 0\nretryMiddleware({\n  retryOn: r => (r.error?.status ?? 0) >= 500 || r.error?.kind === 'network'\n})\n```\n\n**An abort during backoff surfaces as the abort, not the stale result it was retrying.** If the signal driving the request — a whole-operation `timeout`, a caller's own `AbortSignal`, or a dedupe supersede — fires while `retryMiddleware` is sleeping between attempts, the backoff sleep resolves immediately and the loop proceeds straight to the next attempt, which the core fetch rejects instantly (no network call) because the signal is already aborted. The caller receives **that abort** — `kind: 'timeout'` for a deadline, `kind: 'abort'` for a cancellation or a dedupe supersede — never the last real HTTP result (e.g. a stale `503`) that triggered the retry in the first place:\n\n```ts\nconst api = createApi({\n  baseUrl: '/api',\n  requests: {\n    getItems: new Request<Record<string, never>, Item[]>({\n      method: 'GET',\n      path: '/items',\n      timeout: 2000, // whole-operation deadline\n    })\n  },\n  middleware: [retryMiddleware({ max: 5, baseDelay: 1000 })], // long backoff\n})\n\nconst { error } = await api.getItems()\n// If the 2s deadline fires while retryMiddleware is asleep between attempts:\n// error.status === 0, error.kind === 'timeout' -- not the 503 being retried\n```\n\n**`logMiddleware`**\n\nLogs request start and completion to the console with timing:\n\n```\n[apify] → GET getItems /api/items\n[apify] ← getItems OK (142ms)\n\n[apify] → POST createUser /api/users\n[apify] ← createUser ERROR 422 (89ms)\n```\n\nIntended for development. In production, write a custom middleware that sends telemetry to your observability platform.\n\n```ts\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getItems },\n  middleware: [logMiddleware]\n})\n```\n\n**`cacheMiddleware(options?)`**\n\nCaches successful responses in memory, keyed by request name and params. Calls with identical params within the TTL window are served from cache without hitting the network. Each `cacheMiddleware()` call creates an isolated store — different endpoints never share entries.\n\n```ts\nconst getUserCache = cacheMiddleware({ ttl: 5 * 60_000, maxSize: 100 })\n\nconst getUser = new Request<{ id: string }, User>({\n  method: 'GET',\n  path: '/users/:id',\n  middleware: [getUserCache],\n})\n\n// On logout — clear all cached entries:\ngetUserCache.clear()\n\n// Bypass cache for a single call:\nconst { data } = await api.getUser({ id: '42' }, { skipMiddleware: [getUserCache] })\n```\n\nOptions: `ttl` (milliseconds, default 5 min), `maxSize` (max entries, default 50), `debug` (log hits/misses to console, default false). Only successful results are cached — errors always hit the network again.\n\n### Content types\n\nRequest bodies are automatically serialized based on the input type. The `Content-Type` header is set for you unless you explicitly provide one.\n\n| Input type        | Body output        | Content-Type                          |\n| ----------------- | ------------------ | ------------------------------------- |\n| `null`/`undefined` | `null`             | _(none)_                              |\n| `string`          | as-is              | `text/plain`                          |\n| `FormData`        | as-is              | _(browser sets multipart boundary)_   |\n| `URLSearchParams` | as-is              | `application/x-www-form-urlencoded`   |\n| `Blob`            | as-is              | `application/octet-stream`            |\n| `ArrayBuffer`     | as-is              | `application/octet-stream`            |\n| Plain object      | `JSON.stringify()` | `application/json`                    |\n\nHeader merge precedence (most specific wins):\n\n1. **Global headers** (from `createApi` config) -- lowest priority\n2. **Per-request headers** (from `Request` config) -- overrides global\n3. **Per-call headers** (from `CallOptions`) -- highest priority\n\nExplicitly set `Content-Type` headers at any level override the auto-detected value.\n\n### Response parsing\n\nThe `responseType` option on a `Request` determines how the response body is parsed:\n\n| `responseType`  | Method called          | Return type    |\n| --------------- | ---------------------- | -------------- |\n| `'json'`        | `response.text()` then `JSON.parse()` | parsed object  |\n| `'text'`        | `response.text()`      | `string`       |\n| `'blob'`        | `response.blob()`      | `Blob`         |\n| `'arrayBuffer'` | `response.arrayBuffer()` | `ArrayBuffer` |\n| `'formData'`    | `response.formData()`  | `FormData`     |\n| `'none'`        | *(not read -- stream cancelled)* | `undefined` |\n\nThe default is `'json'`. An **empty body under `'json'` is an error**, not a\n`null`: you declared JSON and the server sent none, so there is no value that\ncould honestly satisfy `TResponse`. You get `kind: 'parse'` with the\nresponse's own status (a `204` reports `204`), a non-null `response`, and the\nraw body text -- always `''` for this case -- in `error.body`:\n\n```ts\nconst { data, error } = await api.deleteUser({ id: '42' })\n// 204 No Content, responseType left at the 'json' default:\n// error.kind === 'parse', error.status === 204, data === null\n```\n\nA literal `null` body is **not** empty -- `JSON.parse(\"null\")` is valid JSON,\nand that response still succeeds with `data: null`.\n\n**`responseType: 'none'`** is the declaration for an endpoint that returns no\nbody on success -- a `204`, or a `200` with an empty body, most commonly a\n`DELETE`:\n\n```ts\nconst deleteUser = new Request<{ id: string }, undefined>({\n  method: 'DELETE',\n  path: '/users/:id',\n  responseType: 'none',\n})\n```\n\nNo body is read on a successful (2xx) response: `data` is `undefined`, and any body the server sends anyway is discarded -- its stream is cancelled, so a keep-alive connection is released rather than held open by an unread body. Declare `TResponse` as `undefined` when using `responseType: 'none'` -- but this is a convention, not a compile-time guarantee: `new Request<{ id: string }, User>({ responseType: 'none' })` compiles clean, and if the two disagree, `data` is `undefined` at runtime behind whatever type you declared.\n\n`'none'` only describes the **success** shape. A non-2xx response is still read and parsed as JSON for `error.body` -- an error body is diagnostic (a message, a code) and worth reading even when the caller wants nothing back on success:\n\n```ts\nconst { error } = await api.deleteUser({ id: '42' })\nif (error) {\n  // A 409 { \"error\": \"already deleted\" } still lands in error.body here,\n  // even though deleteUser declares responseType: 'none'.\n  console.error(error.status, error.body)\n}\n```\n\n(3.0.0's advice for this case was to widen the endpoint's `TResponse` to\n`| null`. That advice is superseded: `responseType: 'none'` declares \"no body\"\nrather than \"body or null\", and since 4.0.0 the `| null` workaround no longer\nworks at all -- the empty body is an error before `TResponse` is ever\nconsulted. See [MIGRATION.md](./MIGRATION.md#upgrading-to-400).)\n\n### Cancellation\n\n#### Manual abort via `AbortSignal`\n\nPass an `AbortSignal` through `CallOptions` to cancel a request:\n\n```ts\nconst controller = new AbortController()\n\nconst promise = api.getItems({ page: 1 }, {\n  signal: controller.signal\n})\n\n// Cancel the request\ncontroller.abort()\n\nconst { error } = await promise\n// error.status === 0, error.kind === 'abort', error.body is a DOMException with name 'AbortError'\n```\n\nA cancellation you caused yourself is not reported to `onError` (`kind: 'abort'` is the one kind that's suppressed there) -- see [Error handling with `onError`](#error-handling-with-onerror). It is classified by **provenance**, not by sniffing the thrown value's shape: whatever a middleware or `fetch` actually throws, if it happened because *this request's own signal* aborted, the `Result` is `kind: 'abort'` (or `'timeout'` for a deadline) regardless of the reason's name or type -- a caller-supplied custom abort reason (`controller.abort(new Error('unmounted'))`, or a plain string) still classifies as `'abort'`, not `'network'`.\n\nAborting settles the call even while a middleware is still awaiting work of its own that ignores the signal -- the same backstop that bounds `timeout`, described under [Timeout](#timeout).\n\n#### Auto-cancel via `dedupe`\n\nWhen a `Request` has `dedupe: true`, each new call automatically aborts the previous in-flight call for that endpoint. Identity is per `Request` instance -- different endpoints do not interfere with each other.\n\n```ts\nconst searchUsers = new Request<{ q: string }, User[]>({\n  method: 'GET',\n  path: '/users/search',\n  dedupe: true\n})\n\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { searchUsers }\n})\n\n// Rapid calls -- only the last one completes\napi.searchUsers({ q: 'h' })    // aborted by next call\napi.searchUsers({ q: 'he' })   // aborted by next call\napi.searchUsers({ q: 'hel' })  // this one completes\n```\n\nDedupe and manual abort signals work together. If both are active, the request is cancelled if either fires.\n\n### Timeout\n\nSet `timeout` (milliseconds) on a `Request` or per-call to abort a request that takes too long:\n\n```ts\nconst getUser = new Request<{ id: string }, User>({\n  method: 'GET',\n  path: '/users/:id',\n  timeout: 5000\n})\n\nconst { error } = await api.getUser({ id: '42' })\n// error.status === 0, error.kind === 'timeout' if it fired\n```\n\n**`timeout` is a whole-operation deadline, not a per-attempt budget.** It covers the entire middleware chain, including every retry and every backoff delay. `timeout: 5000` combined with `retryMiddleware(3)` still means \"an answer within 5 seconds\" for the call as a whole — not five seconds for each individual attempt. This is a deliberate choice, and it **differs from axios, XHR, and `got`**, all of which apply a timeout per attempt and therefore let a retrying request run for a multiple of the configured timeout. Know which behavior you're assuming before you tune the number.\n\nIf you want a per-attempt budget instead — the axios-style behavior — write a small signal-replacing middleware and place it *inside* the retry middleware, so a fresh signal is installed on every attempt:\n\n```ts\nconst perAttempt = (ms: number): Middleware => async (ctx, next) => {\n  ctx.request.signal = AbortSignal.timeout(ms)\n  return next()\n}\n\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser },\n  middleware: [retryMiddleware(3), perAttempt(5000)]\n})\n```\n\nBecause middleware order is outermost-to-innermost, `retryMiddleware(3)` re-invokes everything below it — including `perAttempt(5000)` — on every retry, so each attempt gets its own fresh 5-second budget instead of sharing one.\n\nA few more details:\n\n- `CallOptions.timeout` overrides `RequestConfig.timeout` for a single call; a per-call `timeout: 0` disables a per-request timeout rather than falling back to it.\n- A timeout produces an error with `status: 0` and `kind: 'timeout'` — distinguishable from a caller-initiated cancellation (`kind: 'abort'`) and from a genuine network failure (`kind: 'network'`).\n- `result.retry()` always starts a fresh deadline. A retried call is not charged against the original budget.\n- Non-positive or omitted `timeout` disables it entirely (the default).\n- `timeout` composes with `dedupe: true` — the deadline is merged with the dedupe signal rather than discarded by it.\n- Under `share: true` the two timeouts have different owners. `RequestConfig.timeout` belongs to the *operation*: it bounds the one shared request for every caller, measured from when that request started, so a single caller can neither extend it nor disable it with a per-call `timeout: 0`. `CallOptions.timeout` bounds only the caller that passed it — see [Sharing](#sharing).\n\n**The deadline bounds middleware that never looks at the signal, too.** A middleware that awaits something of its own before calling `next()` — a token refresh, say — cannot hold the call past its `timeout`, even if that work never settles:\n\n```ts\nconst auth: Middleware = async (ctx, next) => {\n  const token = await user.getIdToken()   // stalls on a bad network\n  ctx.request.headers.set('Authorization', `Bearer ${token}`)\n  return next()\n}\n// With timeout: 45_000, the call still settles at ~45s: kind 'timeout', status 0.\n```\n\nWhen the deadline passes, the chain gets one macrotask to answer by itself. That is enough for everything that already responds to the abort — `fetch` rejecting, a middleware rethrowing the reason, a fallback middleware that turns a timeout into a cached response — so all of those keep their own `Result` exactly as before. A chain still pending after that is waiting on something the signal does not reach, and the call settles with the same `Result` an aborted `fetch` would have produced: `kind: 'timeout'`, `status: 0`, reported to `onError` once. A caller's own `signal` works the same way, with `kind: 'abort'`, which is not reported.\n\nA promise cannot be cancelled, so the stalled middleware keeps running. Whatever it eventually returns or throws is discarded — no second `Result`, no second `onError` — and if it calls `next()` after the call has settled, no request is sent: `next()` hands back the `Result` the caller already has. To stop the work itself, pass `ctx.request.signal` into it (see [`MiddlewareContext`](#middlewarecontext)).\n\n### Sharing\n\nSet `share: true` on a `Request` to coalesce identical concurrent calls onto a single in-flight request, instead of each caller firing its own:\n\n```ts\nconst getProduct = new Request<{ id: string }, Product>({\n  method: 'GET',\n  path: '/products/:id',\n  share: true\n})\n\n// Only one network request is made; both callers get the same response\nconst [a, b] = await Promise.all([\n  api.getProduct({ id: '42' }),\n  api.getProduct({ id: '42' })\n])\n```\n\n`share` is the sibling of `dedupe`, with the opposite intent: **dedupe cancels** the older call in favor of the newer one, **share joins** the existing call instead of starting a new one. Because the two behaviors contradict each other, setting both on the same `Request` throws at `createApi(...)` time — not at call time — so the mistake surfaces immediately rather than the first time the endpoint is called.\n\n**What counts as \"identical\":** the request name plus a stable serialization of the params. Two calls with the same params to the same endpoint share; different params (or different endpoints) never do.\n\n**What disables sharing for a single call:**\n\n- A per-call `headers` or `middleware` — these change *what* is requested, so handing that caller another caller's response would be a real bug, not just a missed optimization. A call carrying either always gets its own, unshared request.\n- Params that are opaque to the stable serialization — `FormData`, `Blob`, `ArrayBuffer`, `URLSearchParams`, `Date`, `Map`, or `Set` — are never coalesced. The stable serialization used to build the share key can't distinguish two different payloads of these types from each other (it falls back to `Object.keys()`, which is empty for all of them), so two different `FormData` uploads would otherwise collide on the same key and one caller could receive the response meant for the other's payload entirely. Declining to share is always safe; handing back the wrong response never is. A raw `string` is not excluded — it stringifies distinguishably, so a string-param endpoint is soundly coalesced like any other.\n\n**What does *not* disable sharing:** a per-call `signal` or `timeout`. These bound *who is still waiting*, not *what is being asked for*, so they're tracked with a per-caller refcount instead: each sharer's own signal/timeout only removes that caller from the wait list. The underlying request keeps running for everyone else, and is only aborted once every sharer — including the one that gave up — has stopped waiting. A sharer that gives up gets an error `Result` (`kind: 'timeout'` or `kind: 'abort'`), reported to `onError` exactly as the identical non-shared call would be — which means a `'timeout'` give-up reports and an `'abort'` give-up does not (see [Error handling with `onError`](#error-handling-with-onerror)).\n\n**A per-*request* `timeout` is different: it belongs to the operation.** `RequestConfig.timeout` bounds the single shared request itself, measured from when that request started — not from when each caller joined it. Every sharer is therefore bounded by it, a late joiner cannot extend it, and a caller passing `timeout: 0` cannot switch it off for everyone else. Without that, a steadily arriving stream of joiners would keep one socket open indefinitely against a deadline that was supposed to cap it.\n\n```ts\nconst impatient = api.getProduct({ id: '42' }, { timeout: 20 })   // gives up quickly\nconst patient = api.getProduct({ id: '42' })                      // keeps waiting\n\n// impatient's early timeout does not cancel the shared request —\n// patient still gets a real response.\n```\n\n**`result.retry()` on a shared result** re-runs the pipeline using the *acquiring caller's* own per-call options (headers, signal, timeout) — that is, whichever call first started the shared request, not whichever caller happens to invoke `retry()`. This falls out of every non-aborting sharer receiving the literal same `Result` object; it's unavoidable given that design, but worth knowing before relying on it.\n\n**Signal-replacing middleware is safe under `share: true`.** A middleware that installs its own `ctx.request.signal` (a per-attempt timeout, say) does not detach the shared request from the refcount: the refcount signal is merged back in before `fetch`, so the request is still aborted once every sharer has given up.\n\n### TypeScript\n\nType inference flows automatically from `Request` generics through `createApi` to the call site. You never annotate the API methods manually.\n\n```ts\n// 1. Types are declared on the Request\nconst getUser = new Request<{ id: string }, User>({\n  method: 'GET',\n  path: '/users/:id'\n})\n\n// 2. createApi infers method signatures from the requests record\nconst api = createApi({\n  baseUrl: '/api',\n  requests: { getUser }\n})\n\n// 3. Call site is fully typed -- no annotations needed\nconst { data, error } = await api.getUser({ id: '42' })\n//      ^? User | null\n```\n\nThe inference chain works like this:\n\n- `Request<TParams, TResponse>` carries the type info.\n- `createApi` uses internal conditional types to pull the `TParams` and `TResponse` generics from each `Request` instance.\n- A mapped type transforms the requests record into callable methods: each key becomes `(params: TParams, options?: CallOptions) => Promise<Result<TResponse>>`.\n- When `TParams` is `Record<string, never>` (no params), the params argument becomes optional.\n\nAll exported types are available for annotation when needed:\n\n```ts\nimport type {\n  Result,\n  CallOptions,\n  Middleware,\n  MiddlewareContext,\n  MiddlewareNext,\n  RequestConfig,\n  ApiConfig\n} from '@iremlopsum/apify'\n```\n\n## GraphQL Client\n\nUse `createGraphQL` when your backend speaks GraphQL. **Everything in the REST API section applies here too** — `retryMiddleware`, `cacheMiddleware`, `logMiddleware`, `dedupe`, per-call `signal`, `onError`, `retry()`, `skipMiddleware`, header merging — all of it works identically for GraphQL operations. The only difference is transport: every operation is sent as an HTTP POST with `{ query, variables }`.\n\nBoth clients return the same `Result<T>` shape — `result.data`, `result.error`, `result.response`, and `result.retry` work identically.\n\n```ts\nimport { createGraphQL, Operation, gql } from '@iremlopsum/apify'\n\ninterface Category {\n  id: string\n  name: string\n  status: string\n}\n\nconst GET_CATEGORY = gql`\n  query GetCategory($id: String!) {\n    category(id: $id) {\n      id\n      name\n      status\n    }\n  }\n`\n\nconst getCategory = new Operation<{ id: string }, Category>({\n  operation: GET_CATEGORY,\n})\n\nconst graphql = createGraphQL({\n  endpoint: 'https://api.example.com/graphql',\n  operations: { getCategory },\n  onError: (error) => console.error(error.status, error.body),\n})\n\nconst { data, error, response, retry } = await graphql.getCategory({ id: '123' })\n```\n\nOperations with no variables can be called without arguments. Use `Record<string, never>` as `TVariables` to mark an operation as variable-free:\n\n```ts\nconst getViewer = new Operation<Record<string, never>, ViewerData>({ operation: GET_VIEWER })\nconst graphql = createGraphQL({ endpoint, operations: { getViewer } })\n\nconst { data } = await graphql.getViewer() // params argument is optional\n```\n\n### Queries and mutations\n\nWhen you want to distinguish queries from mutations in the client structure, use the `queries` and `mutations` keys instead of `operations`. The `operations` flat shape and the `queries`/`mutations` split are mutually exclusive — TypeScript enforces this at compile time.\n\n```ts\nconst graphql = createGraphQL({\n  endpoint: 'https://api.example.com/graphql',\n  queries: {\n    getCategory: new Operation<{ id: string }, Category>({ operation: GET_CATEGORY }),\n  },\n  mutations: {\n    updateCategory: new Operation<{ id: string; name: string }, Category>({\n      operation: gql`\n        mutation UpdateCategory($id: String!, $name: String!) {\n          updateCategory(id: $id, name: $name) { id name status }\n        }\n      `,\n    }),\n  },\n})\n\ngraphql.query.getCategory({ id: '123' })\ngraphql.mutation.updateCategory({ id: '123', name: 'New Name' })\n```\n\n### GraphQL errors\n\nGraphQL errors (any 2xx with `{ errors: [...] }`) surface as `result.error` with the response's own `status` and `error.body` typed as `GraphQLError[]` — no special handling needed. The same `if (error) { ... }` check covers GraphQL errors, HTTP errors, and network errors uniformly.\n\nGraphQL allows **partial success** -- a nullable field errors while the rest of the query resolves. That data is not discarded: it's available as `error.partialData`, never on `result.data` (which stays `null` whenever `error` is non-null, keeping `Result` a clean discriminated union):\n\n```ts\nconst { error } = await graphql.getCategory({ id: '123' })\nif (error) {\n  console.log(error.body)          // GraphQLError[]\n  console.log(error.partialData)   // whatever `data` the server sent alongside the errors, or undefined\n}\n```\n\nGraphQL's own empty-success rule mirrors the REST client's: a 2xx response carrying neither `data` nor `errors` is `kind: 'parse'`, not a success with `data: null`. This covers an empty body, `{}`, a literal `{\"data\": null}`, and a non-object JSON root -- anything that reaches a 2xx without a `data` or `errors` key. `error.body` holds the raw response text, not a parsed value:\n\n```ts\nconst { error } = await graphql.getCategory({ id: '123' })\nif (error) {\n  console.log(error.kind) // 'parse'\n  console.log(error.body) // raw response text, e.g. '' or '{}'\n}\n```\n\nA `{\"data\": null, \"errors\": [...]}` response is unchanged -- it's still `kind: 'http'`, with any partial result in `error.partialData`, since the GraphQL-errors branch runs first. See [MIGRATION.md](./MIGRATION.md#upgrading-to-400).\n\nOperations support `dedupe: true` in the same way `Request` does — see [Auto-cancel via `dedupe`](#auto-cancel-via-dedupe).\n\n### Middleware\n\n`createGraphQL` accepts the same middleware options as `createApi` — global, per-operation, and per-call — and the `MiddlewareContext` shape is identical, so middleware written for `createApi` works here too.\n\n```ts\nconst graphql = createGraphQL({\n  endpoint: 'https://api.example.com/graphql',\n  operations: { getCategory },\n  middleware: [authMiddleware],\n})\n```\n\n### API Reference additions\n\n| Export              | Kind     | Description                                                            |\n| ------------------- | -------- | ---------------------------------------------------------------------- |\n| `createGraphQL`     | function | Creates a typed GraphQL client from a config of Operation definitions  |\n| `Operation`         | class    | Typed operation definition -- one instance per GraphQL operation       |\n| `gql`               | const    | Tagged template literal for GraphQL documents (editor tooling support) |\n| `OperationConfig`   | type     | Config object for the `Operation` constructor                          |\n| `GraphQLBaseConfig` | type     | Config object for `createGraphQL`                                      |\n| `GraphQLError`      | type     | Shape of a single GraphQL error from `{ errors: [...] }`               |\n\n## Testing\n\n`@iremlopsum/apify/testing` is a separate, framework-agnostic entry point for testing consumers of this library — it has no test-runner dependency, so it works the same under Vitest, Jest, or anything else. It gives you a `fetch` stub with route matching, so your tests exercise the real pipeline — URL building, path substitution, header merging, body serialization, response parsing, your own middleware — rather than stubbing an API method to return a canned `Result` and silently drifting out of sync with what the library actually does.\n\n```ts\nimport { mockFetch, jsonResponse } from '@iremlopsum/apify/testing'\n\nconst mock = mockFetch({\n  'GET /api/users/:id': ({ params }) => jsonResponse({ id: params.id, name: 'Ada' }),\n  'POST /api/users': jsonResponse({ id: 'new-user' }, { status: 201 }),\n})\n\nmock.install()   // replaces globalThis.fetch\n// ... exercise your code, which calls the real api.getUser(...) ...\nmock.restore()    // puts the original globalThis.fetch back\n```\n\nRoutes are keyed as `\"METHOD /path\"`, with `:token` segments captured and handed to a route function as `{ params, request }`. A route value can also be a plain `Response` (built with the `jsonResponse` helper, or your own), or an array of either — the array is consumed one response per matching call, and the final entry repeats once exhausted (handy for \"fail twice, then succeed\").\n\n```ts\nconst mock = mockFetch({\n  'GET /api/flaky': [jsonResponse(null, { status: 503 }), jsonResponse({ ok: true })],\n})\n```\n\n`mock.calls` records every request (`{ method, url, headers, body }`); `mock.callCount('GET /api/users/:id')` and `mock.lastCall(...)` key off the same `\"METHOD /path\"` strings as the routes object.\n\nAn unmatched request makes the stub throw rather than invent a 404 — a mocked test should not quietly pass for a typo'd path. Note what your code actually sees, though: the library catches every `fetch` rejection by design, so that throw arrives as an ordinary `Result` with `error.kind === 'network'` and the `Error` itself as `error.body`, whose message names the method, the URL, and every route that was defined. Assert on the result (or read it in `onError`); do not expect the call to reject.\n\n```ts\nconst r = await api.getUser({ id: '42' })   // routes only define 'GET /api/user/:id'\nexpect(r.error?.kind).toBe('network')\nexpect(String(r.error?.body)).toMatch(/no route matched GET \\/api\\/users\\/42/)\n```\n\nAn empty response array for a route behaves the same way — a descriptive `Error` reaching you as `error.body`, not a bare `TypeError`.\n\nFor stubbing at the `Result` level instead of the `fetch` level, `successResult(data)` and `errorResult(status, body)` build a well-formed `Result` directly (shown here with Vitest's `vi.spyOn`, but any runner's equivalent works the same way):\n\n```ts\nimport { successResult, errorResult } from '@iremlopsum/apify/testing'\n\nvi.spyOn(api, 'getUser').mockResolvedValue(successResult({ id: '42', name: 'Ada' }))\nvi.spyOn(api, 'getUser').mockResolvedValue(errorResult(404, { message: 'not found' }))\n```\n\nA few behaviors worth knowing:\n\n- **`mock.fetch` honours `init.signal`, like real `fetch`.** An already-aborted signal rejects with its `reason`, and so does one that aborts while a route handler is still pending — so a stalled route (`() => new Promise(() => {})`) lets you test your own `timeout` and cancellation handling through the stub. An aborted call is still recorded in `calls` and counted by `callCount`, but does not use up a response from a sequence.\n- **`restore()` assumes `globalThis.fetch` was defined when `install()` ran** — true on Node 20+ (and in every browser), since `fetch` is a global there. If you somehow call `install()` in an environment where `globalThis.fetch` is `undefined` beforehand, `restore()` puts back that `undefined` rather than inventing a real `fetch`.\n- **A route key must be `\"METHOD /path\"`.** A key with no space (`'/users'`) throws at `mockFetch(...)` time, naming the offending key, rather than silently registering a route that can never match.\n- **Declaration order decides when two same-length routes could both match.** Routes are matched in the order they appear in the object you pass to `mockFetch`, and the first structural match wins — put more specific routes first if two patterns could both match the same path.\n- **Trailing and duplicate slashes are normalized away on both sides.** `/a/b/`, `/a//b`, and `/a/b` all match the same route, whether the extra slash is in the route key or in the URL the library actually built.\n\n## Philosophy\n\n### Never throws\n\nEvery API call returns a `Result<T>`. HTTP errors, network failures, parse errors, and even synchronous exceptions during request setup are all captured and returned as structured `{ data, error, response, retry }` objects. No try/catch required at call sites.\n\n### Zero dependencies\n\nThe library uses only the standard `fetch` API and built-in web platform types (`Headers`, `AbortController`, `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer`). There is nothing to install, audit, or bundle beyond the library itself.\n\n### Middleware over interceptors\n\nInstead of separate `onRequest`/`onResponse` interceptor hooks, the library uses a composable onion model where each middleware wraps the next. This means a single function can modify the request, inspect the response, retry on failure, or short-circuit entirely. Three layers (global, per-request, per-call) plus `skipMiddleware` give fine-grained control without configuration complexity.\n\n### Typed dot-access\n\nType safety comes from inference, not annotation. Define `Request<TParams, TResponse>` once, and `createApi` infers everything downstream. The call site (`api.getUser(...)`) is fully typed with zero extra work.\n\n### Runtime-agnostic\n\nNo assumptions about Node.js, browsers, or any specific runtime. If your environment has `fetch`, the library works -- browsers, Node.js 20+, Bun, Deno, React Native, Cloudflare Workers, edge runtimes.\n\n## API Reference\n\n### Core (`@iremlopsum/apify`)\n\n| Export          | Kind     | Description                                                        |\n| --------------- | -------- | ------------------------------------------------------------------ |\n| `createApi`     | function | Creates a typed API client from a config of Request definitions    |\n| `Request`       | class    | Typed endpoint definition -- one instance per endpoint             |\n| `defineRequest` | function | Typed factory — infers path params from the `path` literal, and enforces `responseType: 'none'`. |\n| `paginate` | function | Walks a paginated endpoint, yielding one `Result` per page. |\n| `PaginateOptions` | type | `next`, `maxPages`, and any `CallOptions`. |\n| `StandardSchemaV1` | type | The Standard Schema contract — for typing a helper that takes a validator. |\n| `InferOutput`      | type | The type a schema produces on success. |\n| `StandardIssue`    | type | One validation failure -- the shape of each entry in `error.body` when a schema refuses. |\n| `ApiError`      | class    | Structured error with status, kind, body, headers, and request metadata |\n| `ApiErrorKind`  | type     | `'http' \\| 'network' \\| 'abort' \\| 'timeout' \\| 'parse' \\| 'middleware'` -- discriminates `ApiError.kind` |\n| `RequestConfig` | type     | Config object for the `Request` constructor                        |\n| `ApiConfig`     | type     | Config object for `createApi`                                      |\n| `CallOptions`   | type     | Per-call overrides (`middleware`, `skipMiddleware`, `headers`, `signal`, `timeout`) |\n| `Result`        | type     | Discriminated union of every API call's outcome: `SuccessResult<T> \\| ErrorResult<T>` |\n| `SuccessResult` | type     | The success branch of `Result`: `{ data: T, error: null, response: Response, retry }` |\n| `ErrorResult`   | type     | The error branc","readmeFilename":"README.md"}