├── .changeset ├── README.md └── config.json ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── benchmark.yml │ ├── lint.yml │ ├── pr.yml │ ├── release.yml │ ├── tests.yml │ └── website.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .node-version ├── .npmignore ├── .npmrc ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── benchmark ├── CHANGELOG.md ├── k6.js ├── package.json ├── start-server.ts └── tsconfig.json ├── e2e ├── aws-lambda │ ├── CHANGELOG.md │ ├── package.json │ ├── scripts │ │ ├── bundle.js │ │ ├── createAwsLambdaDeployment.ts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── azure-function │ ├── package.json │ ├── scripts │ │ ├── bundle.js │ │ ├── createAzureFunctionDeployment.ts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── cloudflare-modules │ ├── package.json │ ├── scripts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── cloudflare-workers │ ├── package.json │ ├── scripts │ │ ├── createCfDeployment.ts │ │ └── e2e.ts │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── wrangler.toml ├── shared-scripts │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── runTests.ts │ │ ├── types.ts │ │ └── utils.ts ├── shared-server │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ └── index.ts └── vercel │ ├── .gitignore │ ├── CHANGELOG.md │ ├── next-env.d.ts │ ├── package.json │ ├── scripts │ ├── bundle.js │ ├── createVercelDeployment.ts │ └── e2e.ts │ ├── src │ └── index.ts │ └── tsconfig.json ├── examples ├── auth-example │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── common.ts │ │ └── index.ts │ └── tsconfig.json ├── clickhouse │ ├── CHANGELOG.md │ ├── clickhouse-oas.ts │ ├── index.ts │ ├── package.json │ ├── scripts │ │ └── download-oas.ts │ └── tsconfig.json ├── fireblocks │ ├── CHANGELOG.md │ ├── fireblocks-oas.ts │ ├── index.ts │ ├── package.json │ ├── scripts │ │ └── download-oas.ts │ └── tsconfig.json ├── navitia │ ├── CHANGELOG.md │ ├── index.ts │ ├── navitia-oas.ts │ ├── package.json │ ├── scripts │ │ └── download-oas.ts │ └── tsconfig.json ├── nextjs-example │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── next.svg │ │ ├── thirteen.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ └── api │ │ │ │ └── [...slug] │ │ │ │ └── route.ts │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ └── styles │ │ │ └── globals.css │ └── tsconfig.json ├── soccer-stats │ ├── CHANGELOG.md │ ├── index.ts │ ├── package.json │ ├── soccer-stats-swagger.ts │ └── tsconfig.json ├── spotify │ ├── CHANGELOG.md │ ├── index.ts │ ├── package.json │ ├── scripts │ │ └── download-oas.ts │ ├── spotify-oas.ts │ └── tsconfig.json ├── todolist │ ├── CHANGELOG.md │ ├── __integration_tests__ │ │ ├── __snapshots__ │ │ │ └── todolist.spec.ts.snap │ │ └── todolist.spec.ts │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── client-from-router.ts │ │ ├── index.ts │ │ ├── oas-client.ts │ │ ├── router.ts │ │ └── saved_openapi.ts │ └── tsconfig.json ├── trpc-openapi │ ├── CHANGELOG.md │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── fets │ │ │ └── client.ts │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── api │ │ │ │ ├── [...trpc].ts │ │ │ │ ├── openapi.json.ts │ │ │ │ └── trpc │ │ │ │ │ └── [...trpc].ts │ │ │ ├── index.tsx │ │ │ └── swagger.tsx │ │ └── server │ │ │ ├── database.ts │ │ │ ├── oas.ts │ │ │ ├── openapi.ts │ │ │ └── router.ts │ └── tsconfig.json └── typebox-example │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── client.ts │ └── index.ts │ └── tsconfig.json ├── jest.config.js ├── package.json ├── packages ├── dummy │ ├── package.json │ └── src │ │ └── index.ts └── fets │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ ├── generate-landing-page.cjs │ └── generate-swagger-ui.cjs │ ├── src │ ├── Response.ts │ ├── client │ │ ├── auth │ │ │ └── oauth.ts │ │ ├── clientResponse.ts │ │ ├── createClient.ts │ │ ├── index.ts │ │ ├── plugins │ │ │ └── useClientCookieStore.ts │ │ └── types.ts │ ├── createRouter.ts │ ├── index.ts │ ├── landing-page.html │ ├── plugins │ │ ├── define-routes.ts │ │ ├── formats.ts │ │ ├── openapi.ts │ │ ├── typebox.ts │ │ └── utils.ts │ ├── swagger-ui.html │ ├── typed-fetch.ts │ ├── types.ts │ └── utils.ts │ └── tests │ ├── auth-test.ts │ ├── client-abort.spec.ts │ ├── client │ ├── apiKey-test.ts │ ├── broken-schema-test.ts │ ├── circular-ref-test.ts │ ├── client-exclusive-oas.ts │ ├── client-formdata.test.ts │ ├── client-query-serialization.spec.ts │ ├── default-and-notok-test.ts │ ├── file-uploads.spec.ts │ ├── fixtures │ │ ├── example-apiKey-header-oas.ts │ │ ├── example-broken-schema-oas.ts │ │ ├── example-circular-ref-oas.ts │ │ ├── example-client-query-serialization-oas.ts │ │ ├── example-default-and-notok-oas.ts │ │ ├── example-exclusive-oas.ts │ │ ├── example-form-url-encoded.oas.ts │ │ ├── example-formdata.ts │ │ ├── example-oas.ts │ │ ├── example-oas2.ts │ │ ├── example-spring-oas.ts │ │ └── large-oas.ts │ ├── form-url-encoded-test.ts │ ├── global-params.spec.ts │ ├── large-oas-test.ts │ ├── oas-test.ts │ ├── oas2-test.ts │ ├── plugins │ │ └── client-cookie-store.spec.ts │ └── spring-test.ts │ ├── error-handling.test.ts │ ├── json-schema-test.ts │ ├── plugins │ └── openapi.spec.ts │ ├── router.spec.ts │ ├── typebox-test.ts │ └── typebox.test.ts ├── patches └── jest-leak-detector+29.7.0.patch ├── prettier.config.mjs ├── renovate.json ├── tsconfig.build.json ├── tsconfig.json ├── website ├── .gitignore ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package.json ├── postcss.config.mjs ├── public │ ├── assets │ │ ├── aws-lambda.svg │ │ ├── azure-functions.svg │ │ ├── bun.svg │ │ ├── cloudflare.svg │ │ ├── deno.svg │ │ ├── diagram.svg │ │ ├── ecosystem.svg │ │ ├── fets-logo.svg │ │ ├── fets-text-logo.png │ │ ├── github.svg │ │ ├── google-cloud-functions.svg │ │ ├── json-schema.svg │ │ ├── nextjs.svg │ │ ├── nodejs.svg │ │ ├── openapi.svg │ │ ├── typescript.svg │ │ └── websockets.svg │ └── favicon.ico ├── src │ ├── components │ │ ├── constants.ts │ │ ├── editor.tsx │ │ ├── index-page.tsx │ │ └── theme.ts │ └── pages │ │ ├── _app.tsx │ │ ├── _meta.ts │ │ ├── client │ │ ├── _meta.ts │ │ ├── client-configuration.mdx │ │ ├── error-handling.mdx │ │ ├── inferring-schema-types.mdx │ │ ├── plugins.mdx │ │ ├── quick-start.mdx │ │ └── request-params.mdx │ │ ├── index.mdx │ │ └── server │ │ ├── _meta.ts │ │ ├── comparison.mdx │ │ ├── cookies.mdx │ │ ├── cors.mdx │ │ ├── error-handling.mdx │ │ ├── integrations │ │ ├── _meta.ts │ │ ├── aws-lambda.mdx │ │ ├── azure-functions.mdx │ │ ├── bun.mdx │ │ ├── cloudflare-workers.mdx │ │ ├── deno.mdx │ │ ├── fastify.mdx │ │ ├── gcp.mdx │ │ ├── nextjs.mdx │ │ ├── node-http.mdx │ │ └── uwebsockets.mdx │ │ ├── openapi.mdx │ │ ├── plugins.mdx │ │ ├── programmatic-schemas.mdx │ │ ├── quick-start.mdx │ │ ├── testing.mdx │ │ └── type-safety-and-validation.mdx ├── tailwind.config.ts ├── theme.config.tsx └── tsconfig.json └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/b 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "ardatan/fets" }], 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [], 10 | "snapshot": { 11 | "useCalculatedVersion": true, 12 | "prereleaseTemplate": "{tag}-{datetime}-{commit}" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": ["./tsconfig.json", "website/tsconfig.json"] 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "standard", 9 | "plugin:@typescript-eslint/recommended", 10 | // "plugin:tailwindcss/recommended", 11 | "prettier" 12 | ], 13 | "plugins": ["@typescript-eslint"], 14 | "settings": { 15 | "tailwindcss": { 16 | "config": "website/tailwind.config.cjs" 17 | } 18 | }, 19 | "rules": { 20 | "no-empty": "off", 21 | "no-console": "off", 22 | "no-prototype-builtins": "off", 23 | "no-useless-constructor": "off", 24 | "no-useless-escape": "off", 25 | "no-undef": "off", 26 | "no-dupe-class-members": "off", 27 | "dot-notation": "off", 28 | "no-use-before-define": "off", 29 | "@typescript-eslint/no-unused-vars": "off", 30 | "@typescript-eslint/no-use-before-define": "off", 31 | "@typescript-eslint/no-namespace": "off", 32 | "@typescript-eslint/no-empty-interface": "off", 33 | "@typescript-eslint/no-empty-function": "off", 34 | "@typescript-eslint/no-var-requires": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | "@typescript-eslint/explicit-function-return-type": "off", 38 | "@typescript-eslint/ban-ts-ignore": "off", 39 | "@typescript-eslint/return-await": "error", 40 | "@typescript-eslint/naming-convention": "off", 41 | "@typescript-eslint/interface-name-prefix": "off", 42 | "@typescript-eslint/explicit-module-boundary-types": "off", 43 | "default-param-last": "off", 44 | "@typescript-eslint/ban-types": "off", 45 | "@typescript-eslint/no-empty-object-type": "off", 46 | "import/no-extraneous-dependencies": [ 47 | "error", 48 | { 49 | "devDependencies": ["**/*.test.ts", "**/*.spec.ts"] 50 | } 51 | ], 52 | // conflicts with official prettier-plugin-tailwindcss and tailwind v3 53 | "tailwindcss/classnames-order": "off" 54 | }, 55 | "env": { 56 | "es6": true, 57 | "node": true 58 | }, 59 | "overrides": [ 60 | { 61 | "files": ["**/{test,tests,testing}/**/*.{ts,js}", "*.{spec,test}.{ts,js}"], 62 | "env": { 63 | "jest": true 64 | }, 65 | "rules": { 66 | "@typescript-eslint/no-unused-vars": "off", 67 | "import/no-extraneous-dependencies": "off" 68 | } 69 | } 70 | ], 71 | "ignorePatterns": ["dist", "node_modules", "scripts", "e2e", "benchmark", "next-env.d.ts"], 72 | "globals": { 73 | "BigInt": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ardatan] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | 8 | 9 | 10 | **To Reproduce** Steps to reproduce the behavior: 11 | 12 | 13 | 14 | **Expected behavior** 15 | 16 | 17 | 18 | **Environment:** 19 | 20 | - OS: 21 | - `package-name...`: 22 | - NodeJS: 23 | 24 | **Additional context** 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Have a question? 4 | url: https://github.com/ardatan/fets/discussions/new 5 | about: 6 | Not sure about something? need help from the community? have a question to our team? please 7 | ask and answer questions here. 8 | - name: Any issue with `npm audit` 9 | url: https://overreacted.io/npm-audit-broken-by-design/ 10 | about: 11 | Please do not create issues about `npm audit` and you can contact with us directly for more 12 | questions. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for the core of this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | 8 | 9 | 10 | **Describe the solution you'd like** 11 | 12 | 13 | 14 | **Describe alternatives you've considered** 15 | 16 | 17 | 18 | **Additional context** 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 🚨 **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 9 | 10 | _Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of 11 | the pull request._ 12 | 13 | ## Description 14 | 15 | Please include a summary of the change and which issue is fixed. Please also include relevant 16 | motivation and context. List any dependencies that are required for this change. 17 | 18 | Related # (issue) 19 | 20 | 23 | 24 | ## Type of change 25 | 26 | Please delete options that are not relevant. 27 | 28 | - [ ] Bug fix (non-breaking change which fixes an issue) 29 | - [ ] New feature (non-breaking change which adds functionality) 30 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as 31 | expected) 32 | - [ ] This change requires a documentation update 33 | 34 | ## Screenshots/Sandbox (if appropriate/relevant): 35 | 36 | Adding links to sandbox or providing screenshots can help us understand more about this PR and take 37 | action on it as appropriate 38 | 39 | ## How Has This Been Tested? 40 | 41 | Please describe the tests that you ran to verify your changes. Provide instructions so we can 42 | reproduce. Please also list any relevant details for your test configuration 43 | 44 | - [ ] Test A 45 | - [ ] Test B 46 | 47 | **Test Environment**: 48 | 49 | - OS: 50 | - `package-name`: 51 | - NodeJS: 52 | 53 | ## Checklist: 54 | 55 | - [ ] I have followed the 56 | [CONTRIBUTING](https://github.com/the-guild-org/Stack/blob/master/CONTRIBUTING.md) doc and the 57 | style guidelines of this project 58 | - [ ] I have performed a self-review of my own code 59 | - [ ] I have commented my code, particularly in hard-to-understand areas 60 | - [ ] I have made corresponding changes to the documentation 61 | - [ ] My changes generate no new warnings 62 | - [ ] I have added tests that prove my fix is effective or that my feature works 63 | - [ ] New and existing unit tests and linter rules pass locally with my changes 64 | - [ ] Any dependent changes have been merged and published in downstream modules 65 | 66 | ## Further comments 67 | 68 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose 69 | the solution you did and what alternatives you considered, etc... 70 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'docker' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'daily' 12 | groups: 13 | actions-deps: 14 | patterns: 15 | - '*' 16 | - package-ecosystem: 'github-actions' # See documentation for possible values 17 | directory: '/' # Location of package manifests 18 | schedule: 19 | interval: 'daily' 20 | groups: 21 | actions-deps: 22 | patterns: 23 | - '*' 24 | - package-ecosystem: 'npm' # See documentation for possible values 25 | directory: '/' # Location of package manifests 26 | schedule: 27 | interval: 'daily' 28 | groups: 29 | actions-deps: 30 | patterns: 31 | - '*' 32 | exclude-patterns: 33 | - '@changesets/*' 34 | - 'typescript' 35 | - '^@theguild/' 36 | - 'next' 37 | - 'tailwindcss' 38 | - 'husky' 39 | - '@pulumi/*' 40 | update-types: 41 | - 'minor' 42 | - 'patch' 43 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: benchmark 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - 'website/**' 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-benchmark-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | benchmarks: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | 18 | - name: Setup env 19 | uses: the-guild-org/shared-config/setup@main 20 | with: 21 | nodeVersion: 20 22 | packageManager: yarn 23 | 24 | - name: Build Packages 25 | run: yarn build 26 | 27 | - name: Setup K6 28 | run: | 29 | wget https://github.com/grafana/k6/releases/download/v0.37.0/k6-v0.37.0-linux-amd64.deb 30 | sudo apt-get update 31 | sudo apt-get install ./k6-v0.37.0-linux-amd64.deb 32 | 33 | - name: Start Benchmark 34 | working-directory: ./benchmark 35 | run: | 36 | yarn test 37 | env: 38 | NODE_NO_WARNINGS: true 39 | NODE_ENV: production 40 | GITHUB_PR: ${{ github.event.number }} 41 | GITHUB_SHA: ${{ github.sha }} 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint-prettier 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: {} 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-lint-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | prettier: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Master 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | - name: Setup env 20 | uses: the-guild-org/shared-config/setup@main 21 | with: 22 | nodeVersion: 20 23 | - name: Prettier Check 24 | run: yarn prettier:check 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout Master 29 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 30 | - name: Setup env 31 | uses: the-guild-org/shared-config/setup@main 32 | with: 33 | nodeVersion: 20 34 | - name: ESLint 35 | run: yarn lint 36 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - 'website/**' 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}=pr-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | NODE_OPTIONS: --max-old-space-size=4096 16 | 17 | jobs: 18 | dependencies: 19 | uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main 20 | if: ${{ github.event.pull_request.title != 'Upcoming Release Changes' }} 21 | secrets: 22 | githubToken: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | alpha: 25 | if: 26 | ${{ github.event.pull_request.head.repo.fork != true && github.event.pull_request.title != 27 | 'Upcoming Release Changes' }} 28 | permissions: 29 | contents: read 30 | id-token: write 31 | pull-requests: write 32 | uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main 33 | with: 34 | npmTag: alpha 35 | buildScript: build 36 | nodeVersion: 20 37 | secrets: 38 | githubToken: ${{ secrets.GITHUB_TOKEN }} 39 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 40 | 41 | release-candidate: 42 | if: 43 | ${{ github.event.pull_request.head.repo.full_name == github.repository && 44 | github.event.pull_request.title == 'Upcoming Release Changes' }} 45 | uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main 46 | with: 47 | npmTag: rc 48 | buildScript: build 49 | nodeVersion: 20 50 | restoreDeletedChangesets: true 51 | secrets: 52 | githubToken: ${{ secrets.GITHUB_TOKEN }} 53 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | env: 4 | NODE_OPTIONS: --max-old-space-size=4096 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-release-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | on: 11 | push: 12 | branches: 13 | - master 14 | permissions: write-all 15 | 16 | jobs: 17 | stable: 18 | uses: the-guild-org/shared-config/.github/workflows/release-stable.yml@main 19 | with: 20 | releaseScript: release 21 | nodeVersion: 20 22 | secrets: 23 | githubToken: ${{ secrets.GITHUB_TOKEN }} 24 | npmToken: ${{ secrets.NODE_AUTH_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | permissions: write-all 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-website-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | deployment: 18 | runs-on: ubuntu-latest 19 | if: 20 | github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 21 | 'push' 22 | steps: 23 | - name: checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - uses: the-guild-org/shared-config/setup@main 29 | name: setup env 30 | with: 31 | nodeVersion: 20 32 | packageManager: yarn 33 | 34 | - uses: the-guild-org/shared-config/website-cf@main 35 | name: build and deploy website 36 | env: 37 | NEXT_BASE_PATH: ${{ github.ref == 'refs/heads/master' && '/openapi/fets' || '' }} 38 | SITE_URL: 39 | ${{ github.ref == 'refs/heads/master' && 'https://the-guild.dev/openapi/fets' || '' }} 40 | with: 41 | cloudflareApiToken: ${{ secrets.WEBSITE_CF_API_TOKEN }} 42 | cloudflareAccountId: ${{ secrets.WEBSITE_CF_ACCOUNT_ID }} 43 | githubToken: ${{ secrets.GITHUB_TOKEN }} 44 | projectName: fets 45 | prId: ${{ github.event.pull_request.number }} 46 | websiteDirectory: ./ 47 | buildScript: cd website && yarn build 48 | artifactDir: website/out 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | build 65 | temp 66 | .idea 67 | .bob 68 | .cache 69 | .DS_Store 70 | 71 | test-results/ 72 | junit.xml 73 | 74 | *.tgz 75 | 76 | package-lock.json 77 | 78 | eslint_report.json 79 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v23 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | tests 4 | !dist 5 | .circleci 6 | .prettierrc 7 | bump.js 8 | jest.config.js 9 | tsconfig.json 10 | yarn.lock 11 | yarn-error.log 12 | bundle-test 13 | *.tgz 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | provenance=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | CHANGELOG.md 3 | .next/ 4 | .changeset/*.md 5 | .husky/ 6 | .bob/ 7 | examples/todolist/src/saved_openapi.ts 8 | packages/fets/src/swagger-ui-html.ts 9 | out/ 10 | pnpm-lock.yaml 11 | website/src/components/index-page.tsx 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arda TANRIKULU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

feTS

3 |
Fetch API ❤️ TypeScript
4 |
Fully-featured HTTP framework to build REST APIs with focus on easy setup, performance & great developer experience
5 | Go to documenation 6 |
7 | 8 |
9 | 10 |
11 | 12 | ![npm](https://badgen.net/npm/v/fets) 13 | 14 |
15 | 16 | ## [Documentation](https://www.the-guild.dev/fets) 17 | 18 | Our [documentation website](https://www.the-guild.dev/fets) will help you get started. 19 | 20 | ## [Examples](https://github.com/ardatan/fets/tree/master/examples) 21 | 22 | We've made sure developers can quickly start with feTS by providing a comprehensive set of examples. 23 | [See all of them in the `examples/` folder.](https://github.com/ardatan/fets/tree/master/examples) 24 | 25 | ## Contributing 26 | 27 | If this is your first time contributing to this project, please do read our 28 | [Contributor Workflow Guide](https://github.com/the-guild-org/Stack/blob/master/CONTRIBUTING.md) 29 | before you get started off. 30 | 31 | Feel free to open issues and pull requests. We're always welcome support from the community. 32 | 33 | ## Code of Conduct 34 | 35 | Help us keep feTS open and inclusive. Please read and follow our 36 | [Code of Conduct](https://github.com/the-guild-org/Stack/blob/master/CODE_OF_CONDUCT.md) as adopted 37 | from [Contributor Covenant](https://www.contributor-covenant.org/). 38 | 39 | ## License 40 | 41 | MIT 42 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: ['@babel/plugin-proposal-class-properties'], 7 | }; 8 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fets/benchmark", 3 | "version": "0.0.45", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc", 8 | "check": "exit 0", 9 | "debug": "node --inspect-brk dist/start-server.js", 10 | "loadtest": "k6 -e GITHUB_PR=$GITHUB_PR -e GITHUB_SHA=$GITHUB_SHA -e GITHUB_TOKEN=$GITHUB_TOKEN run k6.js", 11 | "pretest": "npm run build", 12 | "start": "node dist/start-server.js", 13 | "test": "start-server-and-test start http://127.0.0.1:4000/ping loadtest" 14 | }, 15 | "dependencies": { 16 | "fets": "0.8.5" 17 | }, 18 | "devDependencies": { 19 | "@types/k6": "1.0.2", 20 | "start-server-and-test": "2.0.12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /benchmark/start-server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { createRouter, Response, RouterRequest, Type } from 'fets'; 3 | import { App } from 'uWebSockets.js'; 4 | 5 | function handler(request: RouterRequest) { 6 | return request.json().then(body => 7 | Response.json({ 8 | message: `Hello, ${body.name}!`, 9 | }), 10 | ); 11 | } 12 | 13 | let readyCount = 0; 14 | 15 | function greetingsHandler() { 16 | return Response.json({ message: 'Hello, World!' }); 17 | } 18 | 19 | const router = createRouter({}) 20 | .route({ 21 | method: 'GET', 22 | path: '/greetings', 23 | handler: greetingsHandler, 24 | }) 25 | .route({ 26 | method: 'GET', 27 | path: '/greetings-json-schema', 28 | schemas: { 29 | responses: { 30 | 200: Type.Object({ 31 | message: Type.String(), 32 | }), 33 | }, 34 | }, 35 | handler: greetingsHandler, 36 | }) 37 | .route({ 38 | method: 'HEAD', 39 | path: '/ping', 40 | handler: () => 41 | new Response(null, { 42 | status: readyCount === 2 ? 200 : 500, 43 | }), 44 | }) 45 | .route({ 46 | method: 'POST', 47 | path: '/no-schema', 48 | handler, 49 | }) 50 | .route({ 51 | method: 'POST', 52 | path: '/json-schema', 53 | schemas: { 54 | request: { 55 | json: Type.Object({ 56 | name: Type.String(), 57 | }), 58 | }, 59 | responses: { 60 | 200: Type.Object({ 61 | message: Type.String(), 62 | }), 63 | }, 64 | }, 65 | handler, 66 | }); 67 | 68 | createServer(router).listen(4000, () => { 69 | readyCount++; 70 | console.log('listening on 0.0.0.0:4000'); 71 | }); 72 | 73 | App() 74 | .any('/*', router) 75 | .listen('0.0.0.0', 4001, socket => { 76 | if (!socket) { 77 | console.error('failed to listen'); 78 | process.exit(1); 79 | } 80 | readyCount++; 81 | console.log('listening on 0.0.0.0:4001'); 82 | }); 83 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "dist", 6 | "module": "node16", 7 | "moduleResolution": "node16" 8 | }, 9 | "include": ["./start-server.ts"], 10 | "exclude": [] 11 | } 12 | -------------------------------------------------------------------------------- /e2e/aws-lambda/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @e2e/aws-lambda 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | [[`0df1ac7`](https://github.com/ardatan/whatwg-node/commit/0df1ac7d577ba831ce6431d68628b2028c37762f)]: 9 | - @whatwg-node/fetch@0.8.2 10 | 11 | ## 0.0.2 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies []: 16 | - @whatwg-node/fetch@0.8.1 17 | 18 | ## 0.0.1 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies 23 | [[`ea5d252`](https://github.com/ardatan/whatwg-node/commit/ea5d25298c480d4c5483186af41dccda8197164d)]: 24 | - @whatwg-node/fetch@0.8.0 25 | -------------------------------------------------------------------------------- /e2e/aws-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/aws-lambda", 3 | "version": "0.0.3", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/bundle.js", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts" 8 | }, 9 | "dependencies": { 10 | "@e2e/shared-scripts": "0.0.0", 11 | "aws-lambda": "1.0.7" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/aws": "6.81.0", 15 | "@pulumi/awsx": "0.40.1", 16 | "@pulumi/pulumi": "3.173.0", 17 | "@types/aws-lambda": "8.10.149", 18 | "esbuild": "0.25.5", 19 | "ts-node": "10.9.2", 20 | "tsconfig-paths": "4.2.0", 21 | "typescript": "5.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e/aws-lambda/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | 3 | async function main() { 4 | await build({ 5 | entryPoints: ['./src/index.ts'], 6 | outfile: 'dist/index.js', 7 | format: 'cjs', 8 | minify: false, 9 | bundle: true, 10 | platform: 'node', 11 | target: 'es2020', 12 | }); 13 | 14 | console.info(`AWS Lambda build done!`); 15 | } 16 | 17 | main().catch(e => { 18 | console.error(e); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /e2e/aws-lambda/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createAwsLambdaDeployment } from './createAwsLambdaDeployment'; 3 | 4 | runTests(createAwsLambdaDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/aws-lambda/src/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; 2 | import { createTestServerAdapter } from '@e2e/shared-server'; 3 | 4 | const app = createTestServerAdapter(); 5 | 6 | interface ServerContext { 7 | event: APIGatewayEvent; 8 | lambdaContext: Context; 9 | } 10 | 11 | export async function handler( 12 | event: APIGatewayEvent, 13 | lambdaContext: Context, 14 | ): Promise { 15 | const url = new URL(event.path, 'http://localhost'); 16 | if (event.queryStringParameters != null) { 17 | for (const name in event.queryStringParameters) { 18 | const value = event.queryStringParameters[name]; 19 | if (value != null) { 20 | url.searchParams.set(name, value); 21 | } 22 | } 23 | } 24 | 25 | const serverContext: ServerContext = { 26 | event, 27 | lambdaContext, 28 | }; 29 | 30 | const response = await app.fetch( 31 | url, 32 | { 33 | method: event.httpMethod, 34 | headers: event.headers as HeadersInit, 35 | body: event.body 36 | ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') 37 | : undefined, 38 | }, 39 | serverContext, 40 | ); 41 | 42 | const responseHeaders: Record = {}; 43 | 44 | response.headers.forEach((value, name) => { 45 | responseHeaders[name] = value; 46 | }); 47 | 48 | return { 49 | statusCode: response.status, 50 | headers: responseHeaders, 51 | body: await response.text(), 52 | isBase64Encoded: false, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /e2e/aws-lambda/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["./**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/azure-function/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/azure-function", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/bundle.js", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts" 8 | }, 9 | "dependencies": { 10 | "@azure/functions": "4.7.2", 11 | "@e2e/shared-scripts": "0.0.0" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/azure-native": "3.5.0", 15 | "@pulumi/pulumi": "3.173.0", 16 | "esbuild": "0.25.5", 17 | "ts-node": "10.9.2", 18 | "tsconfig-paths": "4.2.0", 19 | "typescript": "5.8.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/azure-function/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const { writeFileSync } = require('fs'); 3 | const { join } = require('path'); 4 | 5 | const projectRoot = join(__dirname, '..'); 6 | 7 | async function main() { 8 | await build({ 9 | entryPoints: [join(projectRoot, './src/index.ts')], 10 | outfile: join(projectRoot, 'dist/fets/index.js'), 11 | format: 'cjs', 12 | minify: false, 13 | bundle: true, 14 | platform: 'node', 15 | target: 'node14', 16 | }); 17 | 18 | writeFileSync( 19 | join(projectRoot, './dist/package.json'), 20 | JSON.stringify({ 21 | name: 'fets-test-function', 22 | version: '0.0.1', 23 | }), 24 | ); 25 | 26 | writeFileSync( 27 | join(projectRoot, './dist/host.json'), 28 | JSON.stringify({ 29 | version: '2.0', 30 | logging: { 31 | applicationInsights: { 32 | samplingSettings: { 33 | isEnabled: true, 34 | excludedTypes: 'Request', 35 | }, 36 | }, 37 | }, 38 | extensionBundle: { 39 | id: 'Microsoft.Azure.Functions.ExtensionBundle', 40 | version: '[2.*, 3.0.0)', 41 | }, 42 | }), 43 | ); 44 | 45 | writeFileSync( 46 | join(projectRoot, './dist/fets/function.json'), 47 | JSON.stringify({ 48 | bindings: [ 49 | { 50 | authLevel: 'anonymous', 51 | type: 'httpTrigger', 52 | direction: 'in', 53 | name: 'req', 54 | methods: ['get', 'post'], 55 | route: '{*segments}', 56 | }, 57 | { 58 | type: 'http', 59 | direction: 'out', 60 | name: 'res', 61 | }, 62 | ], 63 | }), 64 | ); 65 | 66 | console.info(`Azure Function build done!`); 67 | } 68 | 69 | main().catch(e => { 70 | console.error(e); 71 | process.exit(1); 72 | }); 73 | -------------------------------------------------------------------------------- /e2e/azure-function/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createAzureFunctionDeployment } from './createAzureFunctionDeployment'; 3 | 4 | runTests(createAzureFunctionDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/azure-function/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, HttpRequest } from '@azure/functions'; 2 | import { createTestServerAdapter } from '@e2e/shared-server'; 3 | 4 | const app = createTestServerAdapter('/api/fets'); 5 | 6 | export default async function (context: Context, req: HttpRequest): Promise { 7 | context.log('HTTP trigger function processed a request.'); 8 | 9 | try { 10 | const response = await app.fetch(req.url, { 11 | method: req.method?.toString(), 12 | body: req.rawBody, 13 | headers: req.headers, 14 | }); 15 | const responseText = await response.text(); 16 | context.log('feTS response text:', responseText); 17 | 18 | const headersObj: Record = {}; 19 | response.headers.forEach((value, key) => { 20 | headersObj[key] = value; 21 | }); 22 | 23 | context.log('feTS response headers:', headersObj); 24 | context.res = { 25 | status: response.status, 26 | body: responseText, 27 | headers: headersObj, 28 | }; 29 | } catch (e: any) { 30 | context.log.error('Error:', e); 31 | context.res = { 32 | status: 500, 33 | body: e.message, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /e2e/azure-function/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["./**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/cloudflare-modules", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "wrangler deploy --dry-run --outdir=dist", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 8 | "start": "wrangler dev" 9 | }, 10 | "dependencies": { 11 | "@e2e/shared-scripts": "0.0.0" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/cloudflare": "4.16.0", 15 | "@pulumi/pulumi": "3.173.0", 16 | "ts-node": "10.9.2", 17 | "tsconfig-paths": "4.2.0", 18 | "typescript": "5.8.3", 19 | "wrangler": "4.19.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createCfDeployment } from '../../cloudflare-workers/scripts/createCfDeployment'; 3 | 4 | runTests(createCfDeployment('cloudflare-modules', true)) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createTestServerAdapter } from '@e2e/shared-server'; 2 | 3 | export default { 4 | async fetch(request: Request, env: Record, ctx: any): Promise { 5 | const app = createTestServerAdapter(env.WORKER_PATH || '/'); 6 | return app.handle(request, env, ctx); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["./**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-modules/wrangler.toml: -------------------------------------------------------------------------------- 1 | compatibility_date = "2022-02-21" 2 | name = "fets" 3 | account_id = "" 4 | workers_dev = true 5 | main = "src/index.ts" 6 | compatibility_flags = ["streams_enable_constructors"] 7 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/cloudflare-workers", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "wrangler deploy --dry-run --outdir=dist", 7 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 8 | "start": "wrangler dev" 9 | }, 10 | "dependencies": { 11 | "@e2e/shared-scripts": "0.0.0" 12 | }, 13 | "devDependencies": { 14 | "@pulumi/cloudflare": "4.16.0", 15 | "@pulumi/pulumi": "3.173.0", 16 | "ts-node": "10.9.2", 17 | "tsconfig-paths": "4.2.0", 18 | "typescript": "5.8.3", 19 | "wrangler": "4.19.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/scripts/createCfDeployment.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | assertDeployedEndpoint, 4 | DeploymentConfiguration, 5 | env, 6 | execPromise, 7 | fsPromises, 8 | } from '@e2e/shared-scripts'; 9 | import * as cf from '@pulumi/cloudflare'; 10 | import { version } from '@pulumi/cloudflare/package.json'; 11 | import * as pulumi from '@pulumi/pulumi'; 12 | import { Stack } from '@pulumi/pulumi/automation'; 13 | 14 | export function createCfDeployment( 15 | projectName: string, 16 | isModule = false, 17 | ): DeploymentConfiguration<{ 18 | workerUrl: string; 19 | }> { 20 | return { 21 | name: projectName, 22 | prerequisites: async (stack: Stack) => { 23 | console.info('\t\tℹ️ Installing Pulumi CF plugin...'); 24 | // Intall Pulumi CF Plugin 25 | await stack.workspace.installPlugin('cloudflare', version, 'resource'); 26 | 27 | // Build and bundle the worker 28 | console.info('\t\tℹ️ Bundling the CF Worker....'); 29 | await execPromise('yarn build', { 30 | cwd: join(__dirname, '..', '..', projectName), 31 | }); 32 | }, 33 | config: async (stack: Stack) => { 34 | // Configure the Pulumi environment with the CloudFlare credentials 35 | // This will allow Pulummi program to just run without caring about secrets/configs. 36 | // See: https://www.pulumi.com/registry/packages/cloudflare/installation-configuration/ 37 | await stack.setConfig('cloudflare:apiToken', { 38 | value: env('CLOUDFLARE_API_TOKEN'), 39 | }); 40 | await stack.setConfig('cloudflare:accountId', { 41 | value: env('CLOUDFLARE_ACCOUNT_ID'), 42 | }); 43 | }, 44 | program: async () => { 45 | const stackName = pulumi.getStack(); 46 | const workerUrl = `e2e.graphql.yoga/${stackName}`; 47 | 48 | // Deploy CF script as Worker 49 | const workerScript = new cf.WorkerScript('worker', { 50 | content: await fsPromises.readFile( 51 | join(__dirname, '..', '..', projectName, 'dist', 'index.js'), 52 | 'utf-8', 53 | ), 54 | module: isModule, 55 | name: stackName, 56 | plainTextBindings: [ 57 | { 58 | name: 'WORKER_PATH', 59 | text: `/${stackName}`, 60 | }, 61 | ], 62 | }); 63 | 64 | // Create a nice route for easy testing 65 | new cf.WorkerRoute('worker-route', { 66 | scriptName: workerScript.name, 67 | pattern: workerUrl + '*', 68 | zoneId: env('CLOUDFLARE_ZONE_ID'), 69 | }); 70 | 71 | return { 72 | workerUrl: `https://${workerUrl}`, 73 | }; 74 | }, 75 | test: async ({ workerUrl }) => { 76 | console.log(`ℹ️ CloudFlare Worker deployed to URL: ${workerUrl.value}`); 77 | await assertDeployedEndpoint(workerUrl.value); 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createCfDeployment } from './createCfDeployment'; 3 | 4 | runTests(createCfDeployment('cloudflare-workers')) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createTestServerAdapter } from '@e2e/shared-server'; 2 | 3 | const app = createTestServerAdapter((globalThis as any)['WORKER_PATH'] || '/'); 4 | 5 | self.addEventListener('fetch', app); 6 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["./**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/cloudflare-workers/wrangler.toml: -------------------------------------------------------------------------------- 1 | compatibility_date = "2022-02-21" 2 | name = "fets" 3 | account_id = "" 4 | workers_dev = true 5 | main = "src/index.ts" 6 | compatibility_flags = ["streams_enable_constructors"] 7 | -------------------------------------------------------------------------------- /e2e/shared-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/shared-scripts", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@pulumi/pulumi": "3.173.0", 7 | "@types/node": "22.15.29" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/shared-scripts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runTests'; 2 | export * from './types'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /e2e/shared-scripts/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '@pulumi/pulumi'; 2 | import { OutputValue, Stack } from '@pulumi/pulumi/automation'; 3 | 4 | export type DeploymentConfiguration = { 5 | name: string; 6 | prerequisites?: (stack: Stack) => Promise; 7 | config?: (stack: Stack) => Promise; 8 | program: () => Promise<{ 9 | [K in keyof TProgramOutput]: Output | TProgramOutput[K]; 10 | }>; 11 | test: (output: { 12 | [K in keyof TProgramOutput]: Pick & { 13 | value: TProgramOutput[K]; 14 | }; 15 | }) => Promise; 16 | }; 17 | -------------------------------------------------------------------------------- /e2e/shared-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/shared-server", 3 | "version": "0.0.93", 4 | "private": true, 5 | "dependencies": { 6 | "fets": "0.8.5" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /e2e/shared-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, Response, Type } from 'fets'; 2 | 3 | export function createTestServerAdapter(base?: string) { 4 | return createRouter({ 5 | base, 6 | }) 7 | .route({ 8 | method: 'GET', 9 | path: '/greetings/:name', 10 | schemas: { 11 | responses: { 12 | 200: Type.Object({ 13 | message: Type.String(), 14 | }), 15 | }, 16 | }, 17 | handler: req => Response.json({ message: `Hello ${req.params.name}!` }), 18 | }) 19 | .route({ 20 | method: 'POST', 21 | path: '/bye', 22 | schemas: { 23 | request: { 24 | json: Type.Object({ 25 | name: Type.String(), 26 | }), 27 | }, 28 | responses: { 29 | 200: Type.Object({ 30 | message: Type.String(), 31 | }), 32 | }, 33 | }, 34 | handler: async req => { 35 | const { name } = await req.json(); 36 | return Response.json({ message: `Bye ${name}!` }); 37 | }, 38 | }) 39 | .route({ 40 | method: 'GET', 41 | path: '/', 42 | handler: () => 43 | new Response( 44 | ` 45 | 46 | 47 | Platform Agnostic Server 48 | 49 | 50 |

Hello World!

51 | 52 | 53 | `, 54 | { 55 | headers: { 56 | 'Content-Type': 'text/html', 57 | }, 58 | }, 59 | ), 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /e2e/vercel/.gitignore: -------------------------------------------------------------------------------- 1 | pages 2 | -------------------------------------------------------------------------------- /e2e/vercel/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /e2e/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2e/vercel", 3 | "version": "0.0.93", 4 | "private": true, 5 | "scripts": { 6 | "build": "node scripts/bundle.js", 7 | "check": "tsc --pretty --noEmit", 8 | "dev": "next dev", 9 | "e2e": "ts-node -r tsconfig-paths/register scripts/e2e.ts", 10 | "lint": "next lint", 11 | "start": "next start" 12 | }, 13 | "dependencies": { 14 | "@e2e/shared-scripts": "0.0.0", 15 | "@e2e/shared-server": "0.0.93", 16 | "encoding": "0.1.13", 17 | "next": "15.3.3", 18 | "react": "19.1.0", 19 | "react-dom": "19.1.0" 20 | }, 21 | "devDependencies": { 22 | "@pulumi/pulumi": "3.173.0", 23 | "@types/react": "19.1.6", 24 | "esbuild": "0.25.5", 25 | "eslint": "9.28.0", 26 | "eslint-config-next": "15.3.3", 27 | "ts-node": "10.9.2", 28 | "tsconfig-paths": "4.2.0", 29 | "typescript": "5.8.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /e2e/vercel/scripts/bundle.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | 3 | async function main() { 4 | await build({ 5 | entryPoints: ['./src/index.ts'], 6 | outfile: 'pages/api/fets.js', 7 | format: 'cjs', 8 | minify: false, 9 | bundle: true, 10 | platform: 'node', 11 | target: 'es2020', 12 | }); 13 | 14 | console.info(`Vercel Function build done!`); 15 | } 16 | 17 | main().catch(e => { 18 | console.error(e); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /e2e/vercel/scripts/e2e.ts: -------------------------------------------------------------------------------- 1 | import { runTests } from '@e2e/shared-scripts'; 2 | import { createVercelDeployment } from './createVercelDeployment'; 3 | 4 | runTests(createVercelDeployment()) 5 | .then(() => { 6 | process.exit(0); 7 | }) 8 | .catch(err => { 9 | console.error(err); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /e2e/vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createTestServerAdapter } from '@e2e/shared-server'; 2 | 3 | export const config = { 4 | api: { 5 | // Disable body parsing (required for file uploads) 6 | bodyParser: false, 7 | }, 8 | }; 9 | 10 | export default createTestServerAdapter('/api/fets'); 11 | -------------------------------------------------------------------------------- /e2e/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "declaration": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["next-env.d.ts", "./**/*.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/auth-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-auth", 3 | "version": "0.0.17", 4 | "description": "A simple app with Auth", 5 | "private": true, 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@types/node": "22.15.29", 11 | "fets": "0.8.5", 12 | "ts-node": "10.9.2", 13 | "ts-node-dev": "2.0.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/auth-example/src/common.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError, RouterPlugin, Type } from 'fets'; 2 | 3 | export const TOKEN = '1234-5678-9123-4567'; 4 | 5 | export const bearerAuthPlugin: RouterPlugin = { 6 | onRouteHandle({ route, request }) { 7 | if ( 8 | route.security?.find(securityDef => securityDef['myExampleAuth']) && 9 | request.headers.get('authorization') !== `Bearer ${TOKEN}` 10 | ) { 11 | throw new HTTPError( 12 | 401, 13 | 'Unauthorized', 14 | {}, 15 | { 16 | message: 'Invalid bearer token', 17 | }, 18 | ); 19 | } 20 | }, 21 | }; 22 | 23 | export const UnauthorizedSchema = Type.Object({ 24 | message: Type.String(), 25 | }); 26 | -------------------------------------------------------------------------------- /examples/auth-example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import * as crypto from 'node:crypto'; 3 | import { createRouter, Response, Type } from 'fets'; 4 | import { bearerAuthPlugin, UnauthorizedSchema } from './common'; 5 | 6 | export const router = createRouter({ 7 | openAPI: { 8 | components: { 9 | securitySchemes: { 10 | myExampleAuth: { 11 | type: 'http', 12 | scheme: 'bearer', 13 | }, 14 | }, 15 | }, 16 | }, 17 | plugins: [bearerAuthPlugin], 18 | }).route({ 19 | path: '/me', 20 | method: 'GET', 21 | tags: ['Operations for authenticated users'], 22 | security: [ 23 | { 24 | myExampleAuth: {}, 25 | }, 26 | ], 27 | schemas: { 28 | responses: { 29 | 200: Type.Object({ 30 | id: Type.String({ format: 'uuid' }), 31 | name: Type.String(), 32 | }), 33 | 401: UnauthorizedSchema, 34 | }, 35 | }, 36 | handler() { 37 | return Response.json({ 38 | id: crypto.randomUUID(), 39 | name: 'John Doe', 40 | }); 41 | }, 42 | }); 43 | 44 | createServer(router).listen(3000, () => { 45 | console.log('SwaggerUI is served at http://localhost:3000/docs'); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/auth-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules", "dist", "test"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/clickhouse/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import type clickhouseOas from './clickhouse-oas'; 3 | 4 | const clickhouseClient = createClient>({ 5 | endpoint: 'https://api.clickhouse.cloud', 6 | }); 7 | 8 | async function main() { 9 | const res = await clickhouseClient['/v1/organizations/:organizationId/services/:serviceId'].get({ 10 | params: { 11 | organizationId: 'orgId', 12 | serviceId: 'svcId', 13 | }, 14 | }); 15 | if (!res.ok) { 16 | const errBody = await res.json(); 17 | throw new Error(`Request failed: ${errBody.error} : ${errBody.status}`); 18 | } 19 | const body = await res.json(); 20 | console.log(body); 21 | } 22 | 23 | main().catch(err => { 24 | console.error(err); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/clickhouse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-fireblocks", 3 | "version": "0.0.43", 4 | "description": "An example app uses Fireblocks API", 5 | "private": true, 6 | "scripts": { 7 | "prepare": "ts-node scripts/download-oas.ts", 8 | "start": "ts-node-dev index.ts" 9 | }, 10 | "dependencies": { 11 | "@types/node": "22.15.29", 12 | "fets": "0.8.5", 13 | "ts-node": "10.9.2", 14 | "ts-node-dev": "2.0.0", 15 | "typescript": "^5.5.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/clickhouse/scripts/download-oas.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { load as yamlLoad } from 'js-yaml'; 4 | 5 | async function main() { 6 | const res = await fetch('https://api.clickhouse.cloud/v1'); 7 | const yamlData = await res.text(); 8 | if (yamlData) { 9 | const jsonData = yamlLoad(yamlData); 10 | const jsonString = JSON.stringify(jsonData, null, 2); 11 | const exportedJsonString = `/* eslint-disable */ export default ${jsonString} as const;`; 12 | await fsPromises.writeFile(join(__dirname, '..', 'clickhouse-oas.ts'), exportedJsonString); 13 | } else { 14 | throw new Error('No data in yaml file'); 15 | } 16 | } 17 | 18 | main().catch(e => { 19 | console.error(e); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/clickhouse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "paths": { 13 | "fets": ["../../packages/fets/src/index.ts"] 14 | } 15 | }, 16 | "files": ["index.ts"], 17 | "exclude": ["node_modules", "dist", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/fireblocks/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import type fireblocksOas from './fireblocks-oas'; 3 | 4 | const fireblocksClient = createClient>({ 5 | endpoint: 'https://api.fireblocks.io/v1', 6 | }); 7 | 8 | async function main() { 9 | const res = await fireblocksClient['/payments/payout'].post({ 10 | json: { 11 | paymentAccount: { 12 | id: '5f9b3b1f2d5f9d0001c3f5b0', 13 | type: 'VAULT_ACCOUNT', 14 | }, 15 | instructionSet: [], 16 | }, 17 | headers: { 18 | Authorization: `Bearer ${process.env.FIREBLOCKS_API_KEY}`, 19 | }, 20 | }); 21 | 22 | if (!res.ok) { 23 | const { error } = await res.json(); 24 | throw new Error(`Failed ${error.type}, ${error.message}`); 25 | } 26 | 27 | const account = await res.json(); 28 | console.log('Created account', account); 29 | } 30 | 31 | main().catch(err => { 32 | console.error(err); 33 | process.exit(1); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/fireblocks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-clickhouse", 3 | "version": "0.0.38", 4 | "description": "An example app uses ClickHouse Cloud API", 5 | "private": true, 6 | "scripts": { 7 | "prepare": "ts-node scripts/download-oas.ts", 8 | "start": "ts-node-dev index.ts" 9 | }, 10 | "dependencies": { 11 | "@types/node": "22.15.29", 12 | "fets": "0.8.5", 13 | "ts-node": "10.9.2", 14 | "ts-node-dev": "2.0.0", 15 | "typescript": "^5.5.2" 16 | }, 17 | "devDependencies": { 18 | "@types/js-yaml": "4.0.9", 19 | "js-yaml": "4.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/fireblocks/scripts/download-oas.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { load as yamlLoad } from 'js-yaml'; 4 | 5 | async function main() { 6 | const res = await fetch('https://docs.fireblocks.com/api/v1/swagger.yaml'); 7 | const yamlData = await res.text(); 8 | if (yamlData) { 9 | const jsonData = yamlLoad(yamlData); 10 | const jsonString = JSON.stringify(jsonData, null, 2); 11 | const exportedJsonString = `/* eslint-disable */ export default ${jsonString} as const;`; 12 | await fsPromises.writeFile(join(__dirname, '..', 'fireblocks-oas.ts'), exportedJsonString); 13 | } else { 14 | throw new Error('No data in yaml file'); 15 | } 16 | } 17 | 18 | main().catch(e => { 19 | console.error(e); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/fireblocks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "paths": { 13 | "fets": ["../../packages/fets/src/index.ts"] 14 | } 15 | }, 16 | "files": ["index.ts"], 17 | "exclude": ["node_modules", "dist", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/navitia/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import type navitiaOas from './navitia-oas'; 3 | 4 | const navitiaClient = createClient>({ 5 | endpoint: 'https://api.fireblocks.io/v1', 6 | }); 7 | 8 | async function main() { 9 | const res = await navitiaClient['/coord/{lon};{lat}/'].get({ 10 | params: { 11 | lon: 2.3387, 12 | lat: 48.8584, 13 | }, 14 | headers: { 15 | Authorization: 'Basic {token}', 16 | }, 17 | }); 18 | 19 | const result = await res.json(); 20 | console.log(`Address`, result.address); 21 | } 22 | 23 | main().catch(err => { 24 | console.error(err); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/navitia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-navitia", 3 | "version": "0.0.38", 4 | "description": "An example app uses Navitia API", 5 | "private": true, 6 | "scripts": { 7 | "prepare": "ts-node scripts/download-oas.ts", 8 | "start": "ts-node-dev index.ts" 9 | }, 10 | "dependencies": { 11 | "@types/node": "22.15.29", 12 | "fets": "0.8.5", 13 | "ts-node": "10.9.2", 14 | "ts-node-dev": "2.0.0", 15 | "typescript": "^5.5.2" 16 | }, 17 | "devDependencies": { 18 | "@types/js-yaml": "4.0.9", 19 | "js-yaml": "4.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/navitia/scripts/download-oas.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { load as yamlLoad } from 'js-yaml'; 4 | 5 | async function main() { 6 | const res = await fetch('https://api.navitia.io/v1/schema'); 7 | const yamlData = await res.text(); 8 | if (yamlData) { 9 | const jsonData = yamlLoad(yamlData); 10 | const jsonString = JSON.stringify(jsonData, null, 2); 11 | const exportedJsonString = `/* eslint-disable */ export default ${jsonString} as const;`; 12 | await fsPromises.writeFile(join(__dirname, '..', 'navitia-oas.ts'), exportedJsonString); 13 | } else { 14 | throw new Error('No data in yaml file'); 15 | } 16 | } 17 | 18 | main().catch(e => { 19 | console.error(e); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/navitia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "disableSizeLimit": true, 13 | "paths": { 14 | "fets": ["../../packages/fets/src/index.ts"] 15 | } 16 | }, 17 | "files": ["index.ts"], 18 | "exclude": ["node_modules", "dist", "test"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/nextjs-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs-example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with 2 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 3 | 4 | ## Getting Started 5 | 6 | First, run the development server: 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | yarn dev 12 | # or 13 | pnpm dev 14 | ``` 15 | 16 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 17 | 18 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the 19 | file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on 22 | [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in 23 | `pages/api/hello.ts`. 24 | 25 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as 26 | [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 27 | 28 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to 29 | automatically optimize and load Inter, a custom Google Font. 30 | 31 | ## Learn More 32 | 33 | To learn more about Next.js, take a look at the following resources: 34 | 35 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 36 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 37 | 38 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your 39 | feedback and contributions are welcome! 40 | 41 | ## Deploy on Vercel 42 | 43 | The easiest way to deploy your Next.js app is to use the 44 | [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) 45 | from the creators of Next.js. 46 | 47 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more 48 | details. 49 | -------------------------------------------------------------------------------- /examples/nextjs-example/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/nextjs-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-example", 3 | "version": "0.1.62", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@types/node": "22.15.29", 13 | "@types/react": "19.1.6", 14 | "@types/react-dom": "19.1.6", 15 | "fets": "0.8.5", 16 | "next": "15.3.3", 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0", 19 | "typescript": "5.8.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/nextjs-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/feTS/54f7980727361f0cc1d0cec3c9a65f1c1f0169b6/examples/nextjs-example/public/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs-example/public/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /examples/nextjs-example/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/nextjs-example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /examples/nextjs-example/src/app/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, FromSchema, Response, Type } from 'fets'; 2 | 3 | const TODO_SCHEMA = Type.Object({ 4 | id: Type.String(), 5 | content: Type.String(), 6 | }); 7 | 8 | export type Todo = FromSchema; 9 | 10 | const todos: Todo[] = [ 11 | { 12 | id: '1', 13 | content: 'Buy milk', 14 | }, 15 | { 16 | id: '2', 17 | content: 'Buy eggs', 18 | }, 19 | { 20 | id: '3', 21 | content: 'Buy bread', 22 | }, 23 | ]; 24 | 25 | export const router = createRouter({ 26 | base: '/api', 27 | }) 28 | .route({ 29 | method: 'GET', 30 | path: '/todos', 31 | schemas: { 32 | responses: { 33 | 200: Type.Array(TODO_SCHEMA), 34 | }, 35 | }, 36 | handler: () => Response.json(todos), 37 | }) 38 | .route({ 39 | method: 'POST', 40 | path: '/add-todo', 41 | schemas: { 42 | request: { 43 | json: Type.Object({ 44 | content: Type.String(), 45 | }), 46 | }, 47 | responses: { 48 | 201: TODO_SCHEMA, 49 | }, 50 | }, 51 | handler: async req => { 52 | const input = await req.json(); 53 | const todo = { 54 | id: Math.random().toString(36).substring(7), 55 | content: input.content, 56 | }; 57 | todos.push(todo); 58 | return Response.json(todo, { 59 | status: 201, 60 | }); 61 | }, 62 | }); 63 | 64 | export { router as GET, router as POST }; 65 | -------------------------------------------------------------------------------- /examples/nextjs-example/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import '@/styles/globals.css'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/nextjs-example/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/nextjs-example/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import Head from 'next/head'; 3 | import { createClient } from 'fets'; 4 | import type { router, Todo } from '../app/api/[...slug]/route'; 5 | 6 | const client = createClient({ 7 | endpoint: '/api', 8 | }); 9 | 10 | export default function Home() { 11 | const [todos, setTodos] = useState([]); 12 | 13 | useEffect(() => { 14 | client['/todos'] 15 | .get() 16 | .then(res => res.json()) 17 | .then(todos => { 18 | setTodos(todos); 19 | }) 20 | .catch(err => { 21 | alert(`Failed to fetch todos: ${err}`); 22 | }); 23 | }, []); 24 | 25 | const [newTodo, setNewTodo] = useState(''); 26 | return ( 27 | <> 28 | 29 | feTS Example 30 | 31 | 32 | 33 | 34 |
35 |
36 |

37 | Click here to use Swagger UI 38 |

39 |
40 | Add Todo 41 |
42 | 43 | setNewTodo(e.target.value)} 49 | /> 50 | 69 |
70 |
71 |
72 |

73 | Todo List 74 |

75 |
76 |
77 |
    78 | {todos.map(todo => ( 79 |
  • * {todo.content}
  • 80 | ))} 81 |
82 |
83 |
84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /examples/nextjs-example/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: 5 | ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 6 | 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 21 | 22 | --tile-start-rgb: 239, 245, 249; 23 | --tile-end-rgb: 228, 232, 233; 24 | --tile-border: conic-gradient( 25 | #00000080, 26 | #00000040, 27 | #00000030, 28 | #00000020, 29 | #00000010, 30 | #00000010, 31 | #00000080 32 | ); 33 | 34 | --callout-rgb: 238, 240, 241; 35 | --callout-border-rgb: 172, 175, 176; 36 | --card-rgb: 180, 185, 188; 37 | --card-border-rgb: 131, 134, 135; 38 | } 39 | 40 | @media (prefers-color-scheme: dark) { 41 | :root { 42 | --foreground-rgb: 255, 255, 255; 43 | --background-start-rgb: 0, 0, 0; 44 | --background-end-rgb: 0, 0, 0; 45 | 46 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 47 | --secondary-glow: linear-gradient( 48 | to bottom right, 49 | rgba(1, 65, 255, 0), 50 | rgba(1, 65, 255, 0), 51 | rgba(1, 65, 255, 0.3) 52 | ); 53 | 54 | --tile-start-rgb: 2, 13, 46; 55 | --tile-end-rgb: 2, 5, 19; 56 | --tile-border: conic-gradient( 57 | #ffffff80, 58 | #ffffff40, 59 | #ffffff30, 60 | #ffffff20, 61 | #ffffff10, 62 | #ffffff10, 63 | #ffffff80 64 | ); 65 | 66 | --callout-rgb: 20, 20, 20; 67 | --callout-border-rgb: 108, 108, 108; 68 | --card-rgb: 100, 100, 100; 69 | --card-border-rgb: 200, 200, 200; 70 | } 71 | } 72 | 73 | * { 74 | box-sizing: border-box; 75 | padding: 0; 76 | margin: 0; 77 | } 78 | 79 | html, 80 | body { 81 | max-width: 100vw; 82 | overflow-x: hidden; 83 | } 84 | 85 | body { 86 | color: rgb(var(--foreground-rgb)); 87 | background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) 88 | rgb(var(--background-start-rgb)); 89 | margin: auto; 90 | text-align: center; 91 | } 92 | 93 | a { 94 | color: inherit; 95 | text-decoration: none; 96 | } 97 | 98 | @media (prefers-color-scheme: dark) { 99 | html { 100 | color-scheme: dark; 101 | } 102 | } 103 | 104 | fieldset { 105 | padding: 15px; 106 | margin: 30px; 107 | } 108 | -------------------------------------------------------------------------------- /examples/nextjs-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ] 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /examples/soccer-stats/index.ts: -------------------------------------------------------------------------------- 1 | import { createClient, OASModel, type NormalizeOAS } from 'fets'; 2 | import type swagger from './soccer-stats-swagger'; 3 | 4 | type NormalizedOAS = NormalizeOAS; 5 | 6 | const client = createClient({ 7 | endpoint: 'https://api.sportsdata.io/v4/soccer/stats', 8 | }); 9 | 10 | type Area = OASModel; 11 | 12 | function printArea(area: Area) { 13 | console.log(`- ID: ${area.AreaId}, Name: ${area.Name}`); 14 | } 15 | 16 | async function main() { 17 | const res = await client['/{format}/Areas'].get({ 18 | params: { 19 | format: 'json', 20 | }, 21 | headers: { 22 | 'Ocp-Apim-Subscription-Key': 'test', 23 | }, 24 | }); 25 | 26 | if (!res.ok) { 27 | const err = await res.text(); 28 | throw new Error(err); 29 | } 30 | 31 | const data = await res.json(); 32 | console.log(`Areas: ${data.length} items`); 33 | for (const item of data) { 34 | printArea(item); 35 | } 36 | } 37 | 38 | main().catch(err => { 39 | console.error(err); 40 | process.exit(1); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/soccer-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-soccer-stats", 3 | "version": "0.0.30", 4 | "description": "An example app uses Soccer Stats API", 5 | "private": true, 6 | "scripts": { 7 | "start": "ts-node-dev index.ts" 8 | }, 9 | "dependencies": { 10 | "@types/node": "22.15.29", 11 | "fets": "0.8.5", 12 | "ts-node": "10.9.2", 13 | "ts-node-dev": "2.0.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/soccer-stats/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "paths": { 13 | "fets": ["../../packages/fets/src/index.ts"] 14 | } 15 | }, 16 | "files": ["index.ts"], 17 | "exclude": ["node_modules", "dist", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/spotify/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { createClient, type NormalizeOAS } from 'fets'; 3 | import spotifyOas from './spotify-oas'; 4 | 5 | const client = createClient>({ 6 | endpoint: 'https://api.spotify.com/v1', 7 | }); 8 | 9 | async function getToken() { 10 | const clientId = process.env.CLIENT_ID; 11 | if (!clientId) { 12 | throw new Error('Please set CLIENT_ID env'); 13 | } 14 | const clientSecret = process.env.CLIENT_SECRET; 15 | if (!clientSecret) { 16 | throw new Error('Please set CLIENT_SECRET env'); 17 | } 18 | const res = await client['https://accounts.spotify.com/api/token'].post({ 19 | formUrlEncoded: { 20 | grant_type: 'client_credentials', 21 | }, 22 | headers: { 23 | Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, 24 | }, 25 | }); 26 | if (res.ok) { 27 | const data = await res.json(); 28 | return data.access_token; 29 | } 30 | const errData = await res.json(); 31 | throw new Error(errData.error_description); 32 | } 33 | 34 | // For testing purposes 35 | export async function getRecommendations(token: string, artistId: string, trackId: string) { 36 | const res = await client['/recommendations'].get({ 37 | query: { 38 | seed_genres: 'pop', 39 | limit: 3, 40 | seed_artists: artistId, 41 | seed_tracks: trackId, 42 | }, 43 | headers: { 44 | Authorization: `Bearer ${token}`, 45 | }, 46 | }); 47 | if (!res.ok) { 48 | const errData = await res.json(); 49 | throw new Error(errData.error.message); 50 | } 51 | const data = await res.json(); 52 | return data; 53 | } 54 | 55 | export async function createPlaylist(token: string, userId: string, name: string) { 56 | const res = await client['/users/{user_id}/playlists'].post({ 57 | json: { 58 | name, 59 | }, 60 | params: { 61 | user_id: userId, 62 | }, 63 | headers: { 64 | Authorization: `Bearer ${token}`, 65 | }, 66 | }); 67 | if (!res.ok) { 68 | const errData = await res.json(); 69 | throw new Error(errData.error.message); 70 | } 71 | return res.json(); 72 | } 73 | 74 | async function main() { 75 | const token = await getToken(); 76 | const res = await client['/search'].get({ 77 | query: { 78 | q: 'dance monkey', 79 | type: ['track'], 80 | }, 81 | headers: { 82 | Authorization: `Bearer ${token}`, 83 | }, 84 | }); 85 | if (!res.ok) { 86 | const errData = await res.json(); 87 | console.error(errData); 88 | return; 89 | } 90 | const data = await res.json(); 91 | console.table( 92 | data.tracks?.items?.map(item => ({ 93 | artist: item.artists?.map(artist => artist.name).join(', '), 94 | album: item.album?.name, 95 | name: item.name, 96 | })), 97 | ); 98 | } 99 | 100 | main().catch(err => { 101 | console.error(err); 102 | process.exit(1); 103 | }); 104 | -------------------------------------------------------------------------------- /examples/spotify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-spotify", 3 | "version": "0.0.41", 4 | "description": "An example app uses Spotify API", 5 | "private": true, 6 | "scripts": { 7 | "prepare": "ts-node scripts/download-oas.ts", 8 | "start": "ts-node-dev index.ts" 9 | }, 10 | "dependencies": { 11 | "@types/node": "22.15.29", 12 | "dotenv": "16.5.0", 13 | "fets": "0.8.5", 14 | "ts-node": "10.9.2", 15 | "ts-node-dev": "2.0.0", 16 | "typescript": "^5.5.2" 17 | }, 18 | "devDependencies": { 19 | "@types/js-yaml": "4.0.9", 20 | "js-yaml": "4.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/spotify/scripts/download-oas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { promises as fsPromises } from 'node:fs'; 3 | import { join } from 'node:path'; 4 | import { load as yamlLoad } from 'js-yaml'; 5 | 6 | async function main() { 7 | const res = await fetch('https://developer.spotify.com/reference/web-api/open-api-schema.yaml'); 8 | const yamlData = await res.text(); 9 | if (yamlData) { 10 | const jsonData = yamlLoad(yamlData); 11 | const jsonString = JSON.stringify(jsonData); 12 | const exportedJsonString = `/* eslint-disable */ export default ${jsonString} as const;`; 13 | await fsPromises.writeFile(join(__dirname, '..', 'spotify-oas.ts'), exportedJsonString); 14 | } else { 15 | throw new Error('No data in yaml file'); 16 | } 17 | } 18 | 19 | main().catch(e => { 20 | console.error(e); 21 | process.exit(1); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/spotify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "paths": { 13 | "fets": ["../../packages/fets/src/index.ts"] 14 | } 15 | }, 16 | "files": ["index.ts"], 17 | "exclude": ["node_modules", "dist", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /examples/todolist/__integration_tests__/todolist.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { globalAgent } from 'http'; 3 | import { us_socket_local_port } from 'uWebSockets.js'; 4 | import { fetch } from '@whatwg-node/fetch'; 5 | import { app } from '../src/app'; 6 | 7 | describe('TodoList', () => { 8 | let port: number; 9 | beforeAll(done => { 10 | app.listen(0, listenSocket => { 11 | if (!listenSocket) { 12 | done.fail('Failed to start the server'); 13 | return; 14 | } 15 | port = us_socket_local_port(listenSocket); 16 | done(); 17 | }); 18 | }); 19 | afterAll(() => { 20 | app.close(); 21 | globalAgent.destroy(); 22 | }); 23 | it('should work', async () => { 24 | const response = await fetch(`http://localhost:${port}/todos`); 25 | expect(response.status).toBe(200); 26 | await expect(response.json()).resolves.toEqual([]); 27 | }); 28 | it('should show Swagger UI', async () => { 29 | const response = await fetch(`http://localhost:${port}/docs`); 30 | expect(response.status).toBe(200); 31 | expect(response.headers.get('content-type')).toBe('text/html'); 32 | await expect(response.text()).resolves.toContain('SwaggerUI'); 33 | }); 34 | it('should expose OpenAPI document', async () => { 35 | const response = await fetch(`http://localhost:${port}/openapi.json`); 36 | expect(response.status).toBe(200); 37 | expect(response.headers.get('content-type')).toContain('application/json'); 38 | await expect(response.json()).resolves.toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/todolist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todolist", 3 | "version": "0.0.70", 4 | "description": "A simple todo list app", 5 | "private": true, 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@types/node": "22.15.29", 11 | "fets": "0.8.5", 12 | "ts-node": "10.9.2", 13 | "ts-node-dev": "2.0.0", 14 | "typescript": "^5.5.2", 15 | "uWebSockets.js": "uNetworking/uWebSockets.js#semver:^20" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/todolist/src/app.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'uWebSockets.js'; 2 | import { router } from './router'; 3 | 4 | export const app = App().any('/*', router); 5 | -------------------------------------------------------------------------------- /examples/todolist/src/client-from-router.ts: -------------------------------------------------------------------------------- 1 | import { createClient, RouteOutput } from 'fets'; 2 | import type { router } from './router'; 3 | 4 | const sdk = createClient({}); 5 | 6 | const someTodosToAdd = ['Drink coffee', 'Write some code', 'Drink more coffee', 'Write more code']; 7 | 8 | type Todo = RouteOutput; 9 | 10 | (async () => { 11 | const todo: Todo = { 12 | id: '1', 13 | content: 'Drink coffee', 14 | }; 15 | console.log('Inferred type of todo:', todo); 16 | // Adding some todos 17 | for (const todo of someTodosToAdd) { 18 | const addTodoRes = await sdk['/todo'].put({ 19 | json: { 20 | content: todo, 21 | }, 22 | }); 23 | 24 | const addTodoJson = await addTodoRes.json(); 25 | console.log(addTodoJson.id); 26 | } 27 | 28 | // Getting all todos 29 | const getTodosRes = await sdk['/todos'].get(); 30 | const getTodosJson = await getTodosRes.json(); 31 | console.table(getTodosJson); 32 | 33 | // Deleting the first todo 34 | const deleteTodoRes = await sdk['/todo/:id'].delete({ 35 | params: { 36 | id: getTodosJson[0].id, 37 | }, 38 | }); 39 | if (!deleteTodoRes.ok) { 40 | console.error('Failed to delete todo'); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /examples/todolist/src/index.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { app } from './app'; 4 | import { router } from './router'; 5 | 6 | app.listen(3000, () => { 7 | console.log('SwaggerUI is served at http://localhost:3000/docs'); 8 | }); 9 | 10 | const savedOpenAPIFilePath = join(__dirname, 'saved_openapi.ts'); 11 | 12 | // Write the OpenAPI spec to a file 13 | fsPromises 14 | .writeFile( 15 | savedOpenAPIFilePath, 16 | `/* eslint-disable */ 17 | export default ${JSON.stringify(router.openAPIDocument)} as const;`, 18 | ) 19 | .then(() => console.log(`OpenAPI schema is written to ${savedOpenAPIFilePath}`)) 20 | .catch(err => { 21 | console.error(`Could not write OpenAPI schema to file: ${err.message}`); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/todolist/src/oas-client.ts: -------------------------------------------------------------------------------- 1 | import { createClient, OASOutput, type NormalizeOAS } from 'fets'; 2 | import type oas from './saved_openapi'; 3 | 4 | const client = createClient>({ 5 | endpoint: 'http://localhost:3000', 6 | }); 7 | 8 | const someTodosToAdd = ['Drink coffee', 'Write some code', 'Drink more coffee', 'Write more code']; 9 | 10 | type Todo = OASOutput, '/todo/{id}', 'get'>; 11 | 12 | (async () => { 13 | const todo: Todo = { 14 | id: '1', 15 | content: 'Drink coffee', 16 | }; 17 | console.log('Inferred type of todo:', todo); 18 | // Adding some todos 19 | for (const todo of someTodosToAdd) { 20 | const addTodoRes = await client['/todo'].put({ 21 | json: { 22 | content: todo, 23 | }, 24 | }); 25 | 26 | if (!addTodoRes.ok) { 27 | console.error('Failed to add todo'); 28 | break; 29 | } 30 | const addTodoJson = await addTodoRes.json(); 31 | console.log(addTodoJson.id); 32 | } 33 | 34 | // Getting all todos 35 | const getTodosRes = await client['/todos'].get(); 36 | const getTodosJson = await getTodosRes.json(); 37 | console.table(getTodosJson); 38 | 39 | // Deleting the first todo 40 | const deleteTodoRes = await client['/todo/{id}'].delete({ 41 | params: { 42 | id: getTodosJson[0].id, 43 | }, 44 | }); 45 | if (!deleteTodoRes.ok) { 46 | console.error('Failed to delete todo'); 47 | } 48 | })(); 49 | -------------------------------------------------------------------------------- /examples/todolist/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "paths": { 12 | "fets": ["../../packages/fets/src/index.ts"] 13 | } 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /examples/trpc-openapi/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # trpc-openapi-example 2 | 3 | ## 0.1.18 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`4f9c219`](https://github.com/ardatan/feTS/commit/4f9c219e7dc459ce9863a3c923adf084354e6318)]: 8 | - fets@0.8.0 9 | 10 | ## 0.1.17 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`3be42d8`](https://github.com/ardatan/feTS/commit/3be42d8f812c96968a3107aea0c455687bc4930a), [`98d2323`](https://github.com/ardatan/feTS/commit/98d2323c7e20551e99e8b79734ab1e3f7d33cca1), [`5c993ef`](https://github.com/ardatan/feTS/commit/5c993efa9749889df314890d9c03410bcbb11288), [`5c993ef`](https://github.com/ardatan/feTS/commit/5c993efa9749889df314890d9c03410bcbb11288)]: 15 | - fets@0.7.0 16 | 17 | ## 0.1.16 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies 22 | [[`2f1aab9`](https://github.com/ardatan/feTS/commit/2f1aab925375ba7599e0ef994a01a0badecacce1)]: 23 | - fets@0.6.0 24 | 25 | ## 0.1.15 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies 30 | [[`bf99477`](https://github.com/ardatan/feTS/commit/bf99477d36901795fe7e889d8dbba93a60ffe4c4), 31 | [`a350cc6`](https://github.com/ardatan/feTS/commit/a350cc67018fed4f0f33cf3eb0d927223e5e9b72), 32 | [`bf99477`](https://github.com/ardatan/feTS/commit/bf99477d36901795fe7e889d8dbba93a60ffe4c4)]: 33 | - fets@0.5.0 34 | 35 | ## 0.1.14 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies 40 | [[`695c091`](https://github.com/ardatan/feTS/commit/695c0919408c593bff1b16ec99708456aca3bbaf), 41 | [`6217215`](https://github.com/ardatan/feTS/commit/621721559528476e1fa5788f9d5b52c8fec2db87), 42 | [`77d1b25`](https://github.com/ardatan/feTS/commit/77d1b2548b46c80fb15853ff035cada5628a147c), 43 | [`e95fd8f`](https://github.com/ardatan/feTS/commit/e95fd8f824293bc452958ad320a4f1ed5f7eae7c)]: 44 | - fets@0.4.0 45 | 46 | ## 0.1.13 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies 51 | [[`bb0b70b`](https://github.com/ardatan/feTS/commit/bb0b70b3ea66a4b3df18d79e1a7043237be54bf1), 52 | [`bb0b70b`](https://github.com/ardatan/feTS/commit/bb0b70b3ea66a4b3df18d79e1a7043237be54bf1)]: 53 | - fets@0.3.0 54 | 55 | ## 0.1.12 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies 60 | [[`4de0ce6`](https://github.com/ardatan/feTS/commit/4de0ce65bac8fc1b8a2619173dcf962f21cef06a), 61 | [`80c743c`](https://github.com/ardatan/feTS/commit/80c743c9b33a231e86c110452571d1f4c3cd41d2), 62 | [`835b103`](https://github.com/ardatan/feTS/commit/835b103c47f9f1581f19801dfea7b75341860089)]: 63 | - fets@0.2.0 64 | -------------------------------------------------------------------------------- /examples/trpc-openapi/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with 2 | [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 3 | 4 | ## Getting Started 5 | 6 | First, run the development server: 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | yarn dev 12 | # or 13 | pnpm dev 14 | ``` 15 | 16 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 17 | 18 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the 19 | file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on 22 | [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in 23 | `pages/api/hello.ts`. 24 | 25 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as 26 | [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 27 | 28 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to 29 | automatically optimize and load Inter, a custom Google Font. 30 | 31 | ## Learn More 32 | 33 | To learn more about Next.js, take a look at the following resources: 34 | 35 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 36 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 37 | 38 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your 39 | feedback and contributions are welcome! 40 | 41 | ## Deploy on Vercel 42 | 43 | The easiest way to deploy your Next.js app is to use the 44 | [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) 45 | from the creators of Next.js. 46 | 47 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more 48 | details. 49 | -------------------------------------------------------------------------------- /examples/trpc-openapi/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/trpc-openapi/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/trpc-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc-openapi-example", 3 | "version": "0.1.18", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@trpc/server": "^11.0.0", 13 | "fets": "^0.8.0", 14 | "jsonwebtoken": "^9.0.0", 15 | "next": "^15.2.2", 16 | "nextjs-cors": "^2.1.2", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "swagger-ui-react": "^5.0.0", 20 | "trpc-openapi": "^1.1.2", 21 | "zod": "^3.22.4" 22 | }, 23 | "devDependencies": { 24 | "@types/jsonwebtoken": "^9.0.1", 25 | "@types/node": "^22.10.3", 26 | "@types/react": "^19.0.0", 27 | "@types/react-dom": "^19.0.0", 28 | "@types/swagger-ui-react": "^5.18.0", 29 | "@types/uuid": "^10.0.0", 30 | "typescript": "^5.5.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/trpc-openapi/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/feTS/54f7980727361f0cc1d0cec3c9a65f1c1f0169b6/examples/trpc-openapi/public/favicon.ico -------------------------------------------------------------------------------- /examples/trpc-openapi/src/fets/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import { type oas } from '../server/oas'; 3 | 4 | export const client = createClient>({ 5 | endpoint: 'http://localhost:3000/api', 6 | }); 7 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | function MyApp({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/api/[...trpc].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import cors from 'nextjs-cors'; 3 | import { createOpenApiNextHandler } from 'trpc-openapi'; 4 | import { appRouter, createContext } from '../../server/router'; 5 | 6 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 7 | // Setup CORS 8 | await cors(req, res); 9 | 10 | // Handle incoming OpenAPI requests 11 | return createOpenApiNextHandler({ 12 | router: appRouter, 13 | createContext, 14 | })(req, res); 15 | }; 16 | 17 | export default handler; 18 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/api/openapi.json.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { openApiDocument } from '../../server/openapi'; 3 | 4 | // Respond with our OpenAPI schema 5 | const handler = (_req: NextApiRequest, res: NextApiResponse) => { 6 | res.status(200).send(openApiDocument); 7 | }; 8 | 9 | export default handler; 10 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/api/trpc/[...trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from '@trpc/server/adapters/next'; 2 | import { appRouter, createContext } from '../../../server/router'; 3 | 4 | // Handle incoming tRPC requests 5 | export default createNextApiHandler({ 6 | router: appRouter, 7 | createContext, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { NextPage } from 'next'; 3 | import type { NormalizeOAS, OASOutput } from 'fets'; 4 | import { client } from '../fets/client'; 5 | import { oas } from '../server/oas'; 6 | 7 | type Post = OASOutput, '/posts/{id}', 'get'>; 8 | 9 | const Home: NextPage = () => { 10 | const [posts, setPosts] = useState([]); 11 | 12 | useEffect(() => { 13 | const fetchPosts = async () => { 14 | const response = await client['/posts'].get(); 15 | if (!response.ok) throw new Error('Failed to fetch posts'); 16 | return response.json(); 17 | }; 18 | fetchPosts() 19 | .then(res => { 20 | if (res.posts) { 21 | setPosts(res.posts); 22 | } 23 | }) 24 | .catch(console.error); 25 | }, []); 26 | 27 | return ( 28 |
29 |

Posts

30 |
    31 | {posts.map(post => ( 32 |
  • {post.content}
  • 33 | ))} 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Home; 40 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/pages/swagger.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import dynamic from 'next/dynamic'; 3 | import 'swagger-ui-react/swagger-ui.css'; 4 | 5 | const SwaggerUI = dynamic(() => import('swagger-ui-react'), { ssr: false }); 6 | 7 | const Home: NextPage = () => { 8 | // Serve Swagger UI with our OpenAPI schema 9 | return ; 10 | }; 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/server/database.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | email: string; 4 | passcode: number; 5 | name: string; 6 | }; 7 | 8 | export type Post = { 9 | id: string; 10 | content: string; 11 | userId: string; 12 | }; 13 | 14 | export const database: { users: User[]; posts: Post[] } = { 15 | users: [ 16 | { 17 | id: '3dcb4a1f-0c91-42c5-834f-26d227c532e2', 18 | email: 'jb@jamesbe.com', 19 | passcode: 1234, 20 | name: 'James', 21 | }, 22 | { 23 | id: 'ea120573-2eb4-495e-be48-1b2debac2640', 24 | email: 'alex@example.com', 25 | passcode: 9876, 26 | name: 'Alex', 27 | }, 28 | { 29 | id: '2ee1c07c-7537-48f5-b5d8-8740e165cd62', 30 | email: 'sachin@example.com', 31 | passcode: 5678, 32 | name: 'Sachin', 33 | }, 34 | ], 35 | posts: [ 36 | { 37 | id: 'fc206d47-6d50-4b6a-9779-e9eeaee59aa4', 38 | content: 'Hello world', 39 | userId: '3dcb4a1f-0c91-42c5-834f-26d227c532e2', 40 | }, 41 | { 42 | id: 'a10479a2-a397-441e-b451-0b649d15cfd6', 43 | content: 'tRPC is so awesome', 44 | userId: 'ea120573-2eb4-495e-be48-1b2debac2640', 45 | }, 46 | { 47 | id: 'de6867c7-13f1-4932-a69b-e96fd245ee72', 48 | content: 'Know the ropes', 49 | userId: '3dcb4a1f-0c91-42c5-834f-26d227c532e2', 50 | }, 51 | { 52 | id: '15a742b3-82f6-4fba-9fed-2d1328a4500a', 53 | content: 'Fight fire with fire', 54 | userId: 'ea120573-2eb4-495e-be48-1b2debac2640', 55 | }, 56 | { 57 | id: '31afa9ad-bc37-4e74-8d8b-1c1656184a33', 58 | content: 'I ate breakfast today', 59 | userId: '3dcb4a1f-0c91-42c5-834f-26d227c532e2', 60 | }, 61 | { 62 | id: '557cb26a-b26e-4329-a5b4-137327616ead', 63 | content: 'Par for the course', 64 | userId: '2ee1c07c-7537-48f5-b5d8-8740e165cd62', 65 | }, 66 | ], 67 | }; 68 | -------------------------------------------------------------------------------- /examples/trpc-openapi/src/server/openapi.ts: -------------------------------------------------------------------------------- 1 | import { generateOpenApiDocument } from 'trpc-openapi'; 2 | import { appRouter } from './router'; 3 | 4 | // Generate OpenAPI schema document 5 | export const openApiDocument = generateOpenApiDocument(appRouter, { 6 | title: 'Example CRUD API', 7 | description: 'OpenAPI compliant REST API built using tRPC with Next.js', 8 | version: '1.0.0', 9 | baseUrl: 'http://localhost:3000/api', 10 | docsUrl: 'https://github.com/jlalmes/trpc-openapi', 11 | tags: ['auth', 'users', 'posts'], 12 | }); 13 | -------------------------------------------------------------------------------- /examples/trpc-openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "fets": ["../../packages/fets/src/index.ts"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/typebox-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-typebox", 3 | "version": "0.0.70", 4 | "description": "A simple app with TypeBox", 5 | "private": true, 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@types/node": "22.15.29", 11 | "fets": "0.8.5", 12 | "ts-node": "10.9.2", 13 | "ts-node-dev": "2.0.0", 14 | "typescript": "^5.5.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/typebox-example/src/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'fets'; 2 | import type { router } from './index'; 3 | 4 | function assertExp(exp: T, message: string): asserts exp { 5 | if (!exp) { 6 | throw new Error(message); 7 | } 8 | } 9 | 10 | const client = createClient({ 11 | endpoint: 'http://localhost:3000', 12 | }); 13 | 14 | async function main() { 15 | // Add todo 16 | 17 | const addTodoRes = await client['/todos'].put({ 18 | json: { 19 | content: 'Drink coffee', 20 | }, 21 | }); 22 | 23 | const addedTodo = await addTodoRes.json(); 24 | 25 | // Ensure todo is there 26 | 27 | const getTodosRes = await client['/todos/:id'].get({ 28 | params: { 29 | id: addedTodo.id, 30 | }, 31 | }); 32 | 33 | assertExp(getTodosRes.ok, 'Todo not found'); 34 | 35 | const todo = await getTodosRes.json(); 36 | 37 | assertExp(todo.content === 'Drink coffee', 'Todo content is not correct'); 38 | 39 | // Delete todo 40 | 41 | const deleteTodoRes = await client['/todos/:id'].delete({ 42 | params: { 43 | id: addedTodo.id, 44 | }, 45 | }); 46 | 47 | assertExp(deleteTodoRes.ok, 'Failed to delete todo'); 48 | } 49 | 50 | main().catch(console.error); 51 | -------------------------------------------------------------------------------- /examples/typebox-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "lib": ["esnext", "DOM", "DOM.Iterable"], 8 | "allowSyntheticDefaultImports": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "strict": true 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules", "dist", "test"] 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path'); 2 | const { pathsToModuleNameMapper } = require('ts-jest'); 3 | const fs = require('fs'); 4 | const CI = !!process.env.CI; 5 | 6 | const ROOT_DIR = __dirname; 7 | const TSCONFIG = resolve(ROOT_DIR, 'tsconfig.json'); 8 | const tsconfig = require(TSCONFIG); 9 | const ESM_PACKAGES = []; 10 | 11 | const testMatch = ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)']; 12 | testMatch.push(process.env.INTEGRATION_TEST ? '!**/packages/**' : '!**/examples/**'); 13 | 14 | module.exports = { 15 | testEnvironment: 'node', 16 | rootDir: ROOT_DIR, 17 | restoreMocks: true, 18 | reporters: ['default'], 19 | modulePathIgnorePatterns: ['dist', 'test-assets', 'test-files', 'fixtures', 'bun', '.bob'], 20 | moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { 21 | prefix: `${ROOT_DIR}/`, 22 | }), 23 | transformIgnorePatterns: [`node_modules/(?!(${ESM_PACKAGES.join('|')})/)`], 24 | transform: { 25 | '^.+\\.mjs?$': 'babel-jest', 26 | '^.+\\.ts?$': 'babel-jest', 27 | '^.+\\.js$': 'babel-jest', 28 | }, 29 | collectCoverage: false, 30 | cacheDirectory: resolve(ROOT_DIR, `${CI ? '' : 'node_modules/'}.cache/jest`), 31 | resolver: 'bob-the-bundler/jest-resolver', 32 | testMatch, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/dummy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "description": "TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/fets", 9 | "directory": "packages/dummy" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "private": true, 14 | "engines": { 15 | "node": ">=16.0.0" 16 | }, 17 | "main": "dist/cjs/index.js", 18 | "module": "dist/esm/index.js", 19 | "exports": { 20 | ".": { 21 | "require": { 22 | "types": "./dist/typings/index.d.cts", 23 | "default": "./dist/cjs/index.js" 24 | }, 25 | "import": { 26 | "types": "./dist/typings/index.d.ts", 27 | "default": "./dist/esm/index.js" 28 | }, 29 | "default": { 30 | "types": "./dist/typings/index.d.ts", 31 | "default": "./dist/esm/index.js" 32 | } 33 | }, 34 | "./package.json": "./package.json" 35 | }, 36 | "typings": "dist/typings/index.d.ts", 37 | "publishConfig": { 38 | "directory": "dist", 39 | "access": "public" 40 | }, 41 | "sideEffects": false, 42 | "buildOptions": { 43 | "input": "./src/index.ts" 44 | }, 45 | "typescript": { 46 | "definition": "dist/typings/index.d.ts" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/dummy/src/index.ts: -------------------------------------------------------------------------------- 1 | 'I am a dummy package'; 2 | -------------------------------------------------------------------------------- /packages/fets/.gitignore: -------------------------------------------------------------------------------- 1 | swagger-ui-html.ts 2 | landing-page-html.ts 3 | -------------------------------------------------------------------------------- /packages/fets/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /packages/fets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fets", 3 | "version": "0.8.5", 4 | "type": "module", 5 | "description": "TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience", 6 | "repository": { 7 | "type": "git", 8 | "url": "ardatan/fets", 9 | "directory": "packages/fets" 10 | }, 11 | "author": "Arda TANRIKULU ", 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=16.0.0" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "exports": { 19 | ".": { 20 | "require": { 21 | "types": "./dist/typings/index.d.cts", 22 | "default": "./dist/cjs/index.js" 23 | }, 24 | "import": { 25 | "types": "./dist/typings/index.d.ts", 26 | "default": "./dist/esm/index.js" 27 | }, 28 | "default": { 29 | "types": "./dist/typings/index.d.ts", 30 | "default": "./dist/esm/index.js" 31 | } 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "typings": "dist/typings/index.d.ts", 36 | "dependencies": { 37 | "@sinclair/typebox": "^0.34.0", 38 | "@whatwg-node/cookie-store": "^0.2.0", 39 | "@whatwg-node/fetch": "^0.10.0", 40 | "@whatwg-node/server": "^0.10.0", 41 | "hotscript": "^1.0.11", 42 | "json-schema-to-ts": "^3.0.0", 43 | "qs": "^6.13.1", 44 | "ts-toolbelt": "^9.6.0", 45 | "tslib": "^2.3.1" 46 | }, 47 | "devDependencies": { 48 | "@types/express": "^5.0.0", 49 | "@types/qs": "^6.9.8", 50 | "express": "^5.0.0", 51 | "html-minifier-terser": "7.2.0" 52 | }, 53 | "publishConfig": { 54 | "directory": "dist", 55 | "access": "public" 56 | }, 57 | "sideEffects": false, 58 | "buildOptions": { 59 | "input": "./src/index.ts" 60 | }, 61 | "typescript": { 62 | "definition": "dist/typings/index.d.ts" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/fets/scripts/generate-landing-page.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { minify: minifyT } = require('html-minifier-terser'); 4 | 5 | async function minify(str) { 6 | return ( 7 | await minifyT(str, { 8 | minifyJS: true, 9 | useShortDoctype: false, 10 | removeAttributeQuotes: true, 11 | collapseWhitespace: true, 12 | minifyCSS: true, 13 | }) 14 | ).toString(); 15 | } 16 | 17 | async function minifyLandingPage() { 18 | const minified = await minify( 19 | fs.readFileSync(path.join(__dirname, '..', 'src', 'landing-page.html'), 'utf-8'), 20 | ); 21 | 22 | fs.writeFileSync( 23 | path.join(__dirname, '../src/landing-page-html.ts'), 24 | `export default ${JSON.stringify(minified)}`, 25 | ); 26 | } 27 | 28 | minifyLandingPage().catch(err => { 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/fets/scripts/generate-swagger-ui.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { minify: minifyT } = require('html-minifier-terser'); 4 | 5 | async function minify(str) { 6 | return ( 7 | await minifyT(str, { 8 | minifyJS: true, 9 | useShortDoctype: false, 10 | removeAttributeQuotes: true, 11 | collapseWhitespace: true, 12 | minifyCSS: true, 13 | }) 14 | ).toString(); 15 | } 16 | 17 | async function minifySwaggerUI() { 18 | const minified = await minify( 19 | fs.readFileSync(path.join(__dirname, '..', 'src', 'swagger-ui.html'), 'utf-8'), 20 | ); 21 | 22 | fs.writeFileSync( 23 | path.join(__dirname, '../src/swagger-ui-html.ts'), 24 | `export default ${JSON.stringify(minified)}`, 25 | ); 26 | } 27 | 28 | minifySwaggerUI().catch(err => { 29 | console.error(err); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/fets/src/Response.ts: -------------------------------------------------------------------------------- 1 | import { Response as OriginalResponse } from '@whatwg-node/fetch'; 2 | import { StatusCode, TypedResponse, TypedResponseCtor } from './typed-fetch.js'; 3 | 4 | // This allows us to hook into serialization of the response body 5 | /** 6 | * The Response interface of the Fetch API represents the response to a request. 7 | * It contains the status of the response, as well as the response headers, and 8 | * an optional response body. 9 | * 10 | * @param body An object defining a body for the response. This can be null (which is the default value), or a Blob, BufferSource, FormData, Node.js Readable stream, URLSearchParams, or USVString object. The USVString is handled as UTF-8. 11 | * @param options An options object containing any custom settings that you want to apply to the response, or an empty object (which is the default value). 12 | * 13 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Response 14 | */ 15 | export const Response = OriginalResponse as TypedResponseCtor; 16 | 17 | export type Response< 18 | TJSON = any, 19 | THeaders extends Record = Record, 20 | TStatusCode extends StatusCode = StatusCode, 21 | > = TypedResponse; 22 | -------------------------------------------------------------------------------- /packages/fets/src/client/clientResponse.ts: -------------------------------------------------------------------------------- 1 | import { JSONofResponse, TypedResponse } from '../typed-fetch'; 2 | 3 | export type ClientTypedResponsePromise = 4 | Promise & { 5 | json(): Promise : any>; 6 | }; 7 | 8 | export function createClientTypedResponsePromise( 9 | response$: Promise, 10 | ): ClientTypedResponsePromise { 11 | return new Proxy(response$, { 12 | get(target, key, receiver) { 13 | if (key === 'json') { 14 | return () => target.then(res => res.json()); 15 | } 16 | const value = Reflect.get(target, key, receiver); 17 | if (typeof value === 'function') { 18 | return value.bind(target); 19 | } 20 | return value; 21 | }, 22 | has(target, key) { 23 | if (key === 'json') { 24 | return true; 25 | } 26 | return Reflect.has(target, key); 27 | }, 28 | }) as any; 29 | } 30 | -------------------------------------------------------------------------------- /packages/fets/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createClient.js'; 2 | export * from './types.js'; 3 | export * from './plugins/useClientCookieStore.js'; 4 | -------------------------------------------------------------------------------- /packages/fets/src/client/plugins/useClientCookieStore.ts: -------------------------------------------------------------------------------- 1 | import { CookieListItem, CookieStore, parse } from '@whatwg-node/cookie-store'; 2 | import { Headers } from '@whatwg-node/fetch'; 3 | import { ClientPlugin } from '../types'; 4 | 5 | export function useClientCookieStore(cookieStore: CookieStore): ClientPlugin { 6 | return { 7 | async onRequestInit({ requestInit }) { 8 | requestInit.headers = new Headers(requestInit.headers); 9 | let cookieHeader = requestInit.headers.get('cookie') || ''; 10 | if (cookieHeader) { 11 | cookieHeader += '; '; 12 | } 13 | const cookies = await cookieStore.getAll(); 14 | cookieHeader += cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; '); 15 | requestInit.headers.set('cookie', cookieHeader); 16 | }, 17 | onResponse({ response }) { 18 | const setCookies = response.headers.getSetCookie?.(); 19 | if (setCookies) { 20 | for (const setCookie of setCookies) { 21 | const cookieMap = parse(setCookie); 22 | for (const [, cookie] of cookieMap) { 23 | cookieStore.set(cookie as CookieListItem); 24 | } 25 | } 26 | } 27 | }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/fets/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.js'; 2 | export * from './createRouter.js'; 3 | export { URLPattern } from '@whatwg-node/fetch'; 4 | export { useCORS, HTTPError } from '@whatwg-node/server'; 5 | export * from './client/index.js'; 6 | export * from './Response.js'; 7 | export * from '@sinclair/typebox'; 8 | export { registerFormats } from './plugins/formats.js'; 9 | -------------------------------------------------------------------------------- /packages/fets/src/plugins/define-routes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-inner-declarations */ 2 | import { HTTPMethod } from '../typed-fetch'; 3 | import { RouterComponentsBase, RouterPlugin } from '../types'; 4 | 5 | const HTTP_METHODS: HTTPMethod[] = [ 6 | 'GET', 7 | 'HEAD', 8 | 'POST', 9 | 'PUT', 10 | 'DELETE', 11 | 'CONNECT', 12 | 'OPTIONS', 13 | 'TRACE', 14 | 'PATCH', 15 | ]; 16 | 17 | export function useDefineRoutes< 18 | TServerContext, 19 | TComponents extends RouterComponentsBase, 20 | >(): RouterPlugin { 21 | return { 22 | onRoute({ basePath, route, routeByPathByMethod, routeByPatternByMethod, fetchAPI }) { 23 | let fullPath = ''; 24 | if (basePath === '/') { 25 | fullPath = route.path; 26 | } else if (route.path === '/') { 27 | fullPath = basePath; 28 | } else { 29 | fullPath = `${basePath}${route.path}`; 30 | } 31 | if (fullPath.includes(':') || fullPath.includes('*')) { 32 | const pattern = new fetchAPI.URLPattern({ pathname: fullPath }); 33 | function addHandler(method: HTTPMethod) { 34 | let methodPatternMaps = routeByPatternByMethod.get(method); 35 | if (!methodPatternMaps) { 36 | methodPatternMaps = new Map(); 37 | routeByPatternByMethod.set(method, methodPatternMaps); 38 | } 39 | methodPatternMaps.set(pattern, route); 40 | } 41 | if (!route.method) { 42 | for (const method of HTTP_METHODS) { 43 | addHandler(method); 44 | } 45 | } else { 46 | addHandler(route.method); 47 | } 48 | } else { 49 | function addHandler(method: HTTPMethod) { 50 | let methodPathMaps = routeByPathByMethod.get(method); 51 | if (!methodPathMaps) { 52 | methodPathMaps = new Map(); 53 | routeByPathByMethod.set(method, methodPathMaps); 54 | } 55 | methodPathMaps.set(fullPath, route); 56 | } 57 | if (!route.method) { 58 | for (const method of HTTP_METHODS) { 59 | addHandler(method); 60 | } 61 | } else { 62 | addHandler(route.method); 63 | } 64 | } 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/fets/src/plugins/utils.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJECT = {}; 2 | 3 | export function getHeadersObj(headers: Headers): Record { 4 | return new Proxy(EMPTY_OBJECT, { 5 | get(_target, prop: string) { 6 | return headers.get(prop) || undefined; 7 | }, 8 | set(_target, prop: string, value) { 9 | headers.set(prop, value); 10 | return true; 11 | }, 12 | has(_target, prop: string) { 13 | return headers.has(prop); 14 | }, 15 | deleteProperty(_target, prop: string) { 16 | headers.delete(prop); 17 | return true; 18 | }, 19 | ownKeys() { 20 | return [...headers.keys()]; 21 | }, 22 | getOwnPropertyDescriptor() { 23 | return { 24 | enumerable: true, 25 | configurable: true, 26 | }; 27 | }, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/fets/src/swagger-ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SwaggerUI 8 | 41 | 42 | 43 | 44 |
45 | 46 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/fets/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from '@whatwg-node/server'; 2 | 3 | export function asyncIterationUntilReturn( 4 | iterable: Iterable, 5 | callback: (result: TInput) => Promise | TOutput | undefined, 6 | ): Promise | TOutput | undefined { 7 | const iterator = iterable[Symbol.iterator](); 8 | function iterate(): Promise | TOutput | undefined { 9 | const { value, done } = iterator.next(); 10 | if (done) { 11 | return; 12 | } 13 | if (value) { 14 | const callbackResult$ = callback(value); 15 | if (isPromise(callbackResult$)) { 16 | return callbackResult$.then(callbackResult => { 17 | if (callbackResult) { 18 | return callbackResult; 19 | } 20 | return iterate(); 21 | }); 22 | } 23 | if (callbackResult$) { 24 | return callbackResult$; 25 | } 26 | return iterate(); 27 | } 28 | } 29 | return iterate(); 30 | } 31 | 32 | export function isBlob(value: any): value is Blob { 33 | return value.arrayBuffer !== undefined; 34 | } 35 | -------------------------------------------------------------------------------- /packages/fets/tests/auth-test.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@sinclair/typebox'; 2 | import { createRouter } from '../src/createRouter'; 3 | import { Response } from '../src/Response'; 4 | 5 | createRouter({ 6 | openAPI: { 7 | components: { 8 | securitySchemes: { 9 | bearerAuth: { 10 | type: 'http', 11 | scheme: 'bearer', 12 | bearerFormat: 'JWT', 13 | }, 14 | }, 15 | }, 16 | }, 17 | }).route({ 18 | path: '/me', 19 | method: 'GET', 20 | security: [{ bearerAuth: {} }], 21 | schemas: { 22 | responses: { 23 | 200: Type.Object({ 24 | id: Type.String(), 25 | name: Type.String(), 26 | }), 27 | }, 28 | }, 29 | handler() { 30 | return Response.json({ 31 | id: '1', 32 | name: 'John Doe', 33 | }); 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /packages/fets/tests/client-abort.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import type clientQuerySerializationOAS from './client/fixtures/example-client-query-serialization-oas'; 3 | 4 | type NormalizedOAS = NormalizeOAS; 5 | 6 | describe('Client Abort', () => { 7 | it('should abort the request', async () => { 8 | const client = createClient({ 9 | endpoint: 'https://postman-echo.com', 10 | }); 11 | 12 | await expect(client['/get'].get({ signal: AbortSignal.timeout(1) })).rejects.toThrow( 13 | 'The operation was aborted', 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/fets/tests/client/apiKey-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from '../../src'; 2 | import type apiKeyExampleOas from './fixtures/example-apiKey-header-oas'; 3 | 4 | type NormalizedOAS = NormalizeOAS; 5 | const client = createClient({}); 6 | 7 | const res = await client['/me'].get({ 8 | headers: { 9 | 'x-api-key': '123', 10 | }, 11 | }); 12 | 13 | if (!res.ok) { 14 | const errData = await res.json(); 15 | throw new Error(errData.message); 16 | } 17 | const data = await res.json(); 18 | console.info(`User ${data.id}: ${data.name}`); 19 | 20 | const clientWithPredefined = createClient({ 21 | globalParams: { 22 | headers: { 23 | 'x-api-key': '123', 24 | }, 25 | }, 26 | }); 27 | 28 | await clientWithPredefined['/me'].get(); 29 | -------------------------------------------------------------------------------- /packages/fets/tests/client/broken-schema-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, NormalizeOAS } from '../../src/client'; 2 | import type brokenSchemaOas from './fixtures/example-broken-schema-oas'; 3 | 4 | const client = createClient>({}); 5 | 6 | const res = await client['/{id}/meters'].get({ 7 | params: { 8 | id: 1n, 9 | }, 10 | }); 11 | 12 | const data = await res.json(); 13 | 14 | console.log(data[0].id); 15 | 16 | // @ts-expect-error id is not a string 17 | const id: string = data[0].id; 18 | console.log(id); 19 | -------------------------------------------------------------------------------- /packages/fets/tests/client/circular-ref-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createClient, 3 | OASJSONResponseSchema, 4 | OASModel, 5 | OASOutput, 6 | type FromSchema, 7 | type NormalizeOAS, 8 | } from 'fets'; 9 | import type treeOAS from './fixtures/example-circular-ref-oas'; 10 | 11 | // This resolves circular reference correctly 12 | type NormalizedOAS = NormalizeOAS; 13 | 14 | // So it does handle circular reference actually 15 | type SchemaInOAS = 16 | NormalizedOAS['paths']['/tree']['get']['responses']['200']['content']['application/json']['schema']; 17 | 18 | type Test = FromSchema; 19 | 20 | const a: Test = { 21 | number: 1, 22 | child: { 23 | number: 2, 24 | get child() { 25 | return a; 26 | }, 27 | }, 28 | }; 29 | 30 | if (a.child?.child?.child) { 31 | // @ts-expect-error number is a number 32 | a.child.child.child.number = 'a'; 33 | a.child.child.child.number = 1; 34 | } 35 | 36 | type Test2 = FromSchema>; 37 | 38 | const b: Test2 = { 39 | number: 1, 40 | child: { 41 | number: 2, 42 | get child() { 43 | return b; 44 | }, 45 | }, 46 | }; 47 | 48 | type Test3 = OASOutput; 49 | 50 | const c: Test3 = { 51 | number: 1, 52 | child: { 53 | number: 2, 54 | get child() { 55 | return c; 56 | }, 57 | }, 58 | }; 59 | 60 | const client = createClient({}); 61 | 62 | // Somehow here is a problem 63 | const response = await client['/tree'].get(); // <--- HERE THERE IS AN ERROR TS2615 (circular reference for field "child") 64 | 65 | if (response.ok) { 66 | const body = await response.json(); 67 | if (body.child?.child?.child) { 68 | // @ts-expect-error number is a number 69 | body.child.child.child.number = 'a'; 70 | 71 | body.child.child.child.number = 1; 72 | } 73 | } else { 74 | console.log(response.status); 75 | } 76 | 77 | type NodeA = OASModel; 78 | const nodeA = {} as NodeA; 79 | const numberA = nodeA.child?.child?.child?.child?.number; 80 | type NumberA = typeof numberA; 81 | let numberAVar: NumberA; 82 | numberAVar = 2; 83 | // @ts-expect-error - numberAVar is a number 84 | numberAVar = 'a'; 85 | 86 | console.log(numberAVar); 87 | -------------------------------------------------------------------------------- /packages/fets/tests/client/client-exclusive-oas.ts: -------------------------------------------------------------------------------- 1 | import { createClient, NormalizeOAS } from 'fets'; 2 | import exampleExclusiveOas from './fixtures/example-exclusive-oas'; 3 | 4 | const client = createClient>({}); 5 | 6 | const res = await client['/minmaxtest'].post({ 7 | json: { 8 | sequence: 1, 9 | }, 10 | }); 11 | 12 | if (res.ok) { 13 | const successBody = await res.json(); 14 | console.log(successBody.sequence); 15 | } 16 | -------------------------------------------------------------------------------- /packages/fets/tests/client/client-formdata.test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import { File, Request, Response } from '@whatwg-node/fetch'; 3 | import type clientFormDataOAS from './fixtures/example-formdata'; 4 | 5 | describe('Client', () => { 6 | describe('POST', () => { 7 | type NormalizedOAS = NormalizeOAS; 8 | const client = createClient({ 9 | endpoint: 'https://postman-echo.com', 10 | async fetchFn(info, init) { 11 | const request = new Request(info, init); 12 | const formdataReq = await request.formData(); 13 | return Response.json({ 14 | formdata: Object.fromEntries(formdataReq.entries()), 15 | }); 16 | }, 17 | }); 18 | it('handles formdata with non-string values', async () => { 19 | const response = await client['/post'].post({ 20 | formData: { 21 | blob: new File(['foo'], 'foo.txt'), 22 | boolean: true, 23 | number: 42, 24 | }, 25 | }); 26 | const resJson = await response.json(); 27 | expect(resJson.formdata).toMatchObject({ 28 | boolean: 'true', 29 | number: '42', 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/fets/tests/client/client-query-serialization.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import { Request, Response } from '@whatwg-node/fetch'; 3 | import type clientQuerySerializationOAS from './fixtures/example-client-query-serialization-oas'; 4 | 5 | describe('Client', () => { 6 | describe('GET', () => { 7 | type NormalizedOAS = NormalizeOAS; 8 | const client = createClient({ 9 | endpoint: 'https://postman-echo.com', 10 | fetchFn(info, init) { 11 | const request = new Request(info.toString(), init); 12 | return Promise.resolve( 13 | Response.json({ 14 | url: request.url, 15 | }), 16 | ); 17 | }, 18 | }); 19 | it('should support deep objects in query', async () => { 20 | const response = await client['/get'].get({ 21 | query: { 22 | shallow: 'foo', 23 | deep: { 24 | key1: 'bar', 25 | key2: 'baz', 26 | }, 27 | array: ['qux', 'quux'], 28 | }, 29 | }); 30 | 31 | const resJson = await response.json(); 32 | 33 | expect(resJson.url).toBe( 34 | 'https://postman-echo.com/get?shallow=foo&deep%5Bkey1%5D=bar&deep%5Bkey2%5D=baz&array=qux&array=quux', 35 | ); 36 | }); 37 | it('lazily handles json', async () => { 38 | const resJson = await client['/get'].get().json(); 39 | expect(resJson.url).toBe('https://postman-echo.com/get'); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/fets/tests/client/default-and-notok-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import type kratosSchema from './fixtures/example-default-and-notok-oas'; 3 | 4 | export type KratosNormalized = NormalizeOAS; 5 | export const kratos = createClient({ 6 | endpoint: 'http://localhost:4433', 7 | }); 8 | 9 | const response = await kratos['/self-service/registration'].post({ 10 | query: { flow: 'flow-id' }, 11 | json: { 12 | method: 'password', 13 | traits: { email: 'email' }, 14 | password: 'password', 15 | }, 16 | }); 17 | 18 | switch (response.status) { 19 | case 200: { 20 | const json = await response.json(); 21 | console.log(json.session); 22 | // ... 23 | break; 24 | } 25 | case 400: { 26 | const json = await response.json(); 27 | console.log(json.active); 28 | // ... 29 | break; 30 | } 31 | case 410: { 32 | const json = await response.json(); 33 | console.log(json.error?.id); 34 | // ... 35 | break; 36 | } 37 | case 422: { 38 | const json = await response.json(); 39 | console.log(json.redirect_browser_to); 40 | // ... 41 | break; 42 | } 43 | default: { 44 | const otherError = await response.json(); 45 | console.log(otherError.error?.message); 46 | // ... 47 | break; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/fets/tests/client/file-uploads.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient, createRouter, Response } from 'fets'; 2 | import { File } from '@whatwg-node/fetch'; 3 | 4 | describe('File Uploads', () => { 5 | const router = createRouter().route({ 6 | path: '/upload', 7 | method: 'POST', 8 | schemas: { 9 | request: { 10 | formData: { 11 | type: 'object', 12 | properties: { 13 | file: { 14 | type: 'string', 15 | format: 'binary', 16 | }, 17 | }, 18 | required: ['file'], 19 | additionalProperties: false, 20 | }, 21 | }, 22 | responses: { 23 | 200: { 24 | type: 'object', 25 | properties: { 26 | name: { 27 | type: 'string', 28 | }, 29 | size: { 30 | type: 'number', 31 | }, 32 | text: { 33 | type: 'string', 34 | }, 35 | }, 36 | required: ['name', 'size', 'text'], 37 | additionalProperties: false, 38 | }, 39 | }, 40 | }, 41 | async handler(request) { 42 | const formData = await request.formData(); 43 | const file = formData.get('file'); 44 | return Response.json({ 45 | name: file.name, 46 | size: file.size, 47 | text: await file.text(), 48 | }); 49 | }, 50 | }); 51 | const client = createClient({ fetchFn: router.fetch }); 52 | 53 | it('should upload file', async () => { 54 | const file = new File(['hello'], 'hello.txt', { type: 'text/plain' }); 55 | const response = await client['/upload'].post({ 56 | formData: { 57 | file, 58 | }, 59 | }); 60 | expect(response.status).toEqual(200); 61 | expect(await response.json()).toEqual({ 62 | name: 'hello.txt', 63 | size: 5, 64 | text: 'hello', 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-apiKey-header-oas.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | components: { 3 | securitySchemes: { 4 | apiKey: { 5 | type: 'apiKey', 6 | name: 'x-api-key', 7 | in: 'header', 8 | }, 9 | }, 10 | schemas: { 11 | UnauthorizedResponse: { 12 | properties: { 13 | message: { 14 | type: 'string', 15 | }, 16 | }, 17 | type: 'object', 18 | additionalProperties: false, 19 | required: ['message'], 20 | }, 21 | User: { 22 | properties: { 23 | id: { 24 | type: 'integer', 25 | }, 26 | name: { 27 | type: 'string', 28 | }, 29 | }, 30 | type: 'object', 31 | additionalProperties: false, 32 | required: ['id', 'name'], 33 | }, 34 | }, 35 | }, 36 | paths: { 37 | '/me': { 38 | get: { 39 | operationId: 'getMe', 40 | security: [ 41 | { 42 | apiKey: [], 43 | }, 44 | ], 45 | responses: { 46 | '200': { 47 | content: { 48 | 'application/json': { 49 | schema: { 50 | $ref: '#/components/schemas/User', 51 | }, 52 | }, 53 | }, 54 | description: 'OK', 55 | }, 56 | 401: { 57 | content: { 58 | 'application/json': { 59 | schema: { 60 | $ref: '#/components/schemas/UnauthorizedResponse', 61 | }, 62 | }, 63 | }, 64 | description: 'Unauthorized', 65 | }, 66 | }, 67 | summary: 'get me', 68 | tags: ['User'], 69 | }, 70 | }, 71 | }, 72 | } as const; 73 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-broken-schema-oas.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | openapi: '3.0.0', 3 | info: { 4 | title: 'test', 5 | version: '0.1.0', 6 | }, 7 | components: { 8 | schemas: { 9 | Something: { 10 | type: 'object', 11 | properties: { 12 | id: { 13 | type: 'integer', 14 | format: 'int64', 15 | }, 16 | name: { 17 | type: 'string', 18 | }, 19 | }, 20 | required: ['id', 'name'], 21 | additionalProperties: false, 22 | }, 23 | }, 24 | }, 25 | paths: { 26 | '/{id}/meters': { 27 | get: { 28 | parameters: [ 29 | { 30 | name: 'id', 31 | in: 'path', 32 | required: true, 33 | schema: { 34 | type: 'integer', 35 | format: 'int64', 36 | }, 37 | }, 38 | ], 39 | responses: { 40 | '200': { 41 | description: '', 42 | content: { 43 | 'application/json': { 44 | schema: { 45 | type: 'array', 46 | items: { 47 | $ref: '#/components/schemas/Something', 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | default: { 54 | description: '', 55 | }, 56 | }, 57 | security: [ 58 | { 59 | Authorization: [], 60 | }, 61 | ], 62 | }, 63 | }, 64 | securitySchemes: { 65 | Authorization: { 66 | description: 'Requires JWT to access', 67 | type: 'http', 68 | scheme: 'bearer', 69 | bearerFormat: 'bearer', 70 | }, 71 | }, 72 | }, 73 | } as const; 74 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-circular-ref-oas.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | openapi: '3.0.3', 3 | info: { 4 | version: '1', 5 | title: 'Tree - OpenAPI 3.0', 6 | description: 'This is a sample of tree', 7 | termsOfService: 'http://swagger.io/terms/', 8 | }, 9 | paths: { 10 | '/tree': { 11 | get: { 12 | tags: ['tree'], 13 | summary: 'Get tree', 14 | description: '', 15 | operationId: 'getTree', 16 | responses: { 17 | '200': { 18 | description: 'successful operation', 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/components/schemas/Node', 23 | }, 24 | }, 25 | }, 26 | }, 27 | '404': { 28 | description: 'Tree not found', 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | components: { 35 | schemas: { 36 | Node: { 37 | type: 'object', 38 | properties: { 39 | number: { 40 | type: 'integer', 41 | format: 'int64', 42 | example: 10, 43 | }, 44 | child: { 45 | $ref: '#/components/schemas/Node', 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } as const; 52 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-client-query-serialization-oas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | openapi: '3.0.3', 4 | servers: ['https://postman-echo.com'], 5 | paths: { 6 | '/get': { 7 | get: { 8 | summary: 'GET Request', 9 | description: '', 10 | operationId: 'GetGet', 11 | deprecated: 0, 12 | parameters: [ 13 | { 14 | name: 'shallow', 15 | in: 'query', 16 | description: '', 17 | schema: { 18 | type: 'string', 19 | required: false, 20 | }, 21 | }, 22 | { 23 | name: 'deep', 24 | in: 'query', 25 | description: '', 26 | schema: { 27 | type: 'object', 28 | properties: { 29 | key1: { 30 | type: 'string', 31 | example: 'bar', 32 | }, 33 | key2: { 34 | type: 'string', 35 | example: 'baz', 36 | }, 37 | }, 38 | }, 39 | style: 'deepObject', 40 | explode: true, 41 | required: false, 42 | }, 43 | { 44 | name: 'array', 45 | in: 'query', 46 | description: '', 47 | schema: { 48 | type: 'array', 49 | items: { 50 | type: 'string', 51 | }, 52 | }, 53 | required: false, 54 | }, 55 | ], 56 | responses: { 57 | '200': { 58 | description: '', 59 | content: { 60 | 'application/json; charset=utf-8': { 61 | schema: { 62 | type: 'object', 63 | properties: { 64 | url: { 65 | type: 'string', 66 | description: '', 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | } as const; 78 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-exclusive-oas.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | openapi: '3.0.3', 3 | info: { 4 | title: 'Minmaxtest', 5 | description: 'Minmaxtest', 6 | version: '0.1.0', 7 | }, 8 | components: { 9 | securitySchemes: { 10 | basicAuth: { 11 | type: 'http', 12 | scheme: 'basic', 13 | }, 14 | }, 15 | schemas: {}, 16 | }, 17 | paths: { 18 | '/minmaxtest': { 19 | post: { 20 | requestBody: { 21 | content: { 22 | 'application/json': { 23 | schema: { 24 | type: 'object', 25 | properties: { 26 | sequence: { 27 | type: 'integer', 28 | exclusiveMinimum: true, 29 | minimum: 0, 30 | }, 31 | }, 32 | required: ['sequence'], 33 | additionalProperties: false, 34 | }, 35 | }, 36 | }, 37 | required: true, 38 | }, 39 | responses: { 40 | '201': { 41 | description: 'Default Response', 42 | content: { 43 | 'application/json': { 44 | schema: { 45 | type: 'object', 46 | properties: { 47 | sequence: { 48 | type: 'integer', 49 | exclusiveMinimum: true, 50 | minimum: 0, 51 | }, 52 | }, 53 | required: ['sequence'], 54 | additionalProperties: false, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | } as const; 64 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-form-url-encoded.oas.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | paths: { 3 | '/test': { 4 | post: { 5 | operationId: 'test', 6 | requestBody: { 7 | content: { 8 | 'application/x-www-form-urlencoded': { 9 | schema: { 10 | type: 'object', 11 | properties: { 12 | name: { 13 | type: 'string', 14 | }, 15 | age: { 16 | type: 'integer', 17 | }, 18 | }, 19 | required: ['name', 'age'], 20 | additionalProperties: false, 21 | }, 22 | }, 23 | }, 24 | required: true, 25 | }, 26 | responses: { 27 | '200': { 28 | content: { 29 | 'application/json': { 30 | schema: { 31 | type: 'object', 32 | properties: { 33 | id: { 34 | type: 'integer', 35 | }, 36 | }, 37 | required: ['id'], 38 | additionalProperties: false, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | } as const; 48 | -------------------------------------------------------------------------------- /packages/fets/tests/client/fixtures/example-formdata.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | openapi: '3.0.3', 4 | servers: ['https://postman-echo.com'], 5 | paths: { 6 | '/post': { 7 | post: { 8 | requestBody: { 9 | content: { 10 | 'multipart/form-data': { 11 | schema: { 12 | type: 'object', 13 | properties: { 14 | blob: { 15 | type: 'string', 16 | format: 'binary', 17 | }, 18 | boolean: { 19 | type: 'boolean', 20 | }, 21 | number: { 22 | type: 'number', 23 | }, 24 | }, 25 | additionalProperties: false, 26 | required: ['blob', 'boolean', 'number'], 27 | }, 28 | }, 29 | }, 30 | required: true, 31 | }, 32 | responses: { 33 | '200': { 34 | description: '', 35 | content: { 36 | 'application/json; charset=utf-8': { 37 | schema: { 38 | type: 'object', 39 | properties: { 40 | formdata: { 41 | type: 'object', 42 | properties: { 43 | blob: { 44 | type: 'string', 45 | }, 46 | boolean: { 47 | type: 'boolean', 48 | }, 49 | number: { 50 | type: 'number', 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | } as const; 64 | -------------------------------------------------------------------------------- /packages/fets/tests/client/form-url-encoded-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from '../../src'; 2 | import type formUrlEncodedOas from './fixtures/example-form-url-encoded.oas'; 3 | 4 | const client = createClient>({}); 5 | 6 | const res = await client['/test'].post({ 7 | formUrlEncoded: { 8 | name: 'test', 9 | age: 18, 10 | }, 11 | }); 12 | 13 | if (!res.ok) { 14 | throw new Error('not ok'); 15 | } 16 | -------------------------------------------------------------------------------- /packages/fets/tests/client/global-params.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient, createRouter, Response } from 'fets'; 2 | 3 | describe('Client Global Params', () => { 4 | it('should pass global params', async () => { 5 | const router = createRouter().route({ 6 | path: '/test', 7 | method: 'GET', 8 | handler: req => 9 | Response.json({ 10 | headers: Object.fromEntries(req.headers.entries()), 11 | query: req.query, 12 | }), 13 | }); 14 | const client = createClient({ 15 | fetchFn: router.fetch, 16 | globalParams: { 17 | headers: { 18 | 'x-api-key': '123', 19 | }, 20 | query: { 21 | foo: 'bar', 22 | }, 23 | }, 24 | }); 25 | 26 | const res = await client['/test'].get(); 27 | 28 | expect(res.status).toBe(200); 29 | const data = await res.json(); 30 | expect(data.headers['x-api-key']).toBe('123'); 31 | expect(data.query['foo']).toBe('bar'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/fets/tests/client/large-oas-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from '../../src'; 2 | import oas from './fixtures/large-oas'; 3 | 4 | export const client = createClient>({ 5 | endpoint: 'http://localhost:3000/api', 6 | }); 7 | 8 | const usersRes = await client['/users'].get(); 9 | 10 | if (!usersRes.ok) { 11 | throw new Error('Failed to get users'); 12 | } 13 | 14 | const usersResJson = await usersRes.json(); 15 | console.log(usersResJson.users[0].id); 16 | -------------------------------------------------------------------------------- /packages/fets/tests/client/oas-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from '../../src/client'; 2 | import oas from './fixtures/example-oas'; 3 | 4 | const client = createClient>({}); 5 | 6 | const getAllTodosRes = await client['/todos'].get(); 7 | 8 | if (!getAllTodosRes.ok) { 9 | throw new Error('Failed to get todos'); 10 | } 11 | 12 | const todos = await getAllTodosRes.json(); 13 | 14 | const firstTodo = todos[0]; 15 | 16 | firstTodo.id = '123'; 17 | firstTodo.content = 'Hello world'; 18 | // @ts-expect-error - completed is not a property on Todo 19 | firstTodo.completed = true; 20 | 21 | const getTodo = await client['/todo/{id}'].get({ 22 | params: { 23 | id: '1', 24 | // @ts-expect-error - foo is not a parameter 25 | foo: 'bar', 26 | }, 27 | }); 28 | 29 | const todo = await getTodo.json(); 30 | 31 | // @ts-expect-error - it can be an error response 32 | todo.id = '123'; 33 | 34 | // @ts-expect-error - it can be a success response 35 | todo.message = 'Hello world'; 36 | 37 | if (getTodo.ok) { 38 | const successResponse = await getTodo.json(); 39 | successResponse.id = '123'; 40 | successResponse.content = 'Hello world'; 41 | // @ts-expect-error - completed is not a property on Todo 42 | successResponse.completed = true; 43 | } else { 44 | const errorResponse = await getTodo.json(); 45 | // @ts-expect-error - it cannot be a success response 46 | errorResponse.id = '123'; 47 | errorResponse.message = 'Hello world'; 48 | } 49 | 50 | const getTodo2 = await client['/todo/{id}.json'].get({ 51 | params: { 52 | id: '1', 53 | // @ts-expect-error - foo is not a parameter 54 | foo: 'bar', 55 | }, 56 | }); 57 | 58 | const todo2 = await getTodo2.json(); 59 | 60 | // @ts-expect-error - it can be an error response 61 | todo2.id = '123'; 62 | 63 | // @ts-expect-error - it can be a success response 64 | todo2.message = 'Hello world'; 65 | 66 | if (getTodo2.ok) { 67 | const successResponse = await getTodo2.json(); 68 | successResponse.id = '123'; 69 | successResponse.content = 'Hello world'; 70 | // @ts-expect-error - completed is not a property on Todo 71 | successResponse.completed = true; 72 | } else { 73 | const errorResponse = await getTodo2.json(); 74 | // @ts-expect-error - it cannot be a success response 75 | errorResponse.id = '123'; 76 | errorResponse.message = 'Hello world'; 77 | } 78 | 79 | const uploadRes = await client['/upload'].post({ 80 | formData: { 81 | file: new File(['Hello world'], 'hello.txt'), 82 | description: 'Greetings', 83 | licensed: true, 84 | }, 85 | }); 86 | 87 | const uploadJson = await uploadRes.json(); 88 | console.log(uploadJson.name); 89 | console.log(uploadJson.description); 90 | console.log(uploadJson.type); 91 | console.log(uploadJson.size); 92 | console.log(uploadJson.lastModified); 93 | -------------------------------------------------------------------------------- /packages/fets/tests/client/oas2-test.ts: -------------------------------------------------------------------------------- 1 | // This OpenAPI schema has `parameters` under each endpoint in `paths` instead of method objects 2 | // And it also has `v1.User` schema which has dots in it 3 | import { createClient, type NormalizeOAS } from '../../src'; 4 | import type exampleOAS2 from './fixtures/example-oas2'; 5 | 6 | const client = createClient>({}); 7 | 8 | const res = await client['/api/v1/user/{userID}'].get({ 9 | params: { 10 | userID: '1', 11 | }, 12 | }); 13 | 14 | if (!res.ok) { 15 | const error = await res.json(); 16 | throw new Error(`Failed to get user: ${error.msg} (${error.request_id})`); 17 | } 18 | 19 | const user = await res.json(); 20 | console.log(user.id); 21 | -------------------------------------------------------------------------------- /packages/fets/tests/client/plugins/client-cookie-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { CookieStore } from '@whatwg-node/cookie-store'; 2 | import { Headers, Response } from '@whatwg-node/fetch'; 3 | import { createClient } from '../../../src/client/createClient'; 4 | import { useClientCookieStore } from '../../../src/client/plugins/useClientCookieStore'; 5 | 6 | describe('useClientCookieStore', () => { 7 | it('should work', async () => { 8 | const cookieStore = new CookieStore('foo=bar'); 9 | let receivedCookie = ''; 10 | const client = createClient({ 11 | async fetchFn(_, init) { 12 | const headers = new Headers(init?.headers); 13 | receivedCookie = headers.get('cookie') ?? ''; 14 | return new Response('test', { 15 | status: 200, 16 | headers: { 17 | 'set-cookie': 'test=1', 18 | }, 19 | }); 20 | }, 21 | plugins: [useClientCookieStore(cookieStore)], 22 | }); 23 | await client['/test'].get(); 24 | expect(receivedCookie).toBe('foo=bar'); 25 | const responseCookie = await cookieStore.get('test'); 26 | expect(responseCookie?.value).toBe('1'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/fets/tests/client/spring-test.ts: -------------------------------------------------------------------------------- 1 | import { createClient, type NormalizeOAS } from 'fets'; 2 | import exampleSpringOas from './fixtures/example-spring-oas'; 3 | 4 | const client = createClient>({ 5 | endpoint: 'http://localhost:8080', 6 | }); 7 | 8 | const createUserRes = await client['/user'].post({ 9 | json: { 10 | username: 'test', 11 | password: 'test', 12 | }, 13 | }); 14 | 15 | if (!createUserRes.ok) { 16 | throw new Error('failed'); 17 | } 18 | 19 | const createdUser = await createUserRes.json(); 20 | 21 | const newUsername: string = createdUser.username!; 22 | console.log('newUsername', newUsername); 23 | 24 | console.log({ 25 | id: createdUser.id, 26 | username: createdUser.username, 27 | // @ts-expect-error a property is missing 28 | a: createdUser.a, 29 | }); 30 | 31 | const createPetRes = await client['/pet'].post({ 32 | json: { 33 | name: 'test', 34 | photoUrls: [], 35 | // @ts-expect-error a property is missing 36 | a: 1, 37 | }, 38 | headers: { 39 | Authorization: 'Bearer token', 40 | }, 41 | }); 42 | 43 | if (!createPetRes.ok) { 44 | throw new Error(`failed with status: ${createPetRes.status} ${createPetRes.statusText}}`); 45 | } 46 | 47 | const createdPet = await createPetRes.json(); 48 | 49 | const newPetName: string = createdPet.name; 50 | console.log('newPetName', newPetName); 51 | 52 | console.log({ 53 | id: createdPet.id, 54 | name: createdPet.name, 55 | // @ts-expect-error a property is missing 56 | a: createdPet.a, 57 | }); 58 | 59 | // @ts-expect-error headers are missing 60 | client['/pet'].post({ 61 | json: { 62 | name: 'test', 63 | photoUrls: [], 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /packages/fets/tests/error-handling.test.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError } from '@whatwg-node/server'; 2 | import { createRouter } from '../src/createRouter'; 3 | 4 | describe('Error Handling', () => { 5 | it('does not leak internal errors', async () => { 6 | const router = createRouter({}).route({ 7 | path: '/test', 8 | method: 'GET', 9 | handler() { 10 | throw new Error('Some Internal Error'); 11 | }, 12 | }); 13 | const response = await router.fetch('http://localhost:3000/test'); 14 | expect(response.status).toBe(500); 15 | const result = await response.text(); 16 | expect(result).toBe(''); 17 | }); 18 | it('handles HTTPError', async () => { 19 | const router = createRouter({}).route({ 20 | path: '/test', 21 | method: 'GET', 22 | handler() { 23 | throw new HTTPError( 24 | 412, 25 | 'Some HTTP Error', 26 | { 27 | 'x-foo': 'bar', 28 | }, 29 | { 30 | extra: 'data', 31 | }, 32 | ); 33 | }, 34 | }); 35 | const response = await router.fetch('http://localhost:3000/test'); 36 | expect(response.status).toBe(412); 37 | const result = await response.json(); 38 | expect(result).toMatchObject({ 39 | extra: 'data', 40 | }); 41 | expect(response.headers.get('x-foo')).toBe('bar'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/fets/tests/plugins/openapi.spec.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, Response } from 'fets'; 2 | 3 | describe('OpenAPI spec', () => { 4 | it('respects base path', async () => { 5 | const router = createRouter({ 6 | base: '/api', 7 | }).route({ 8 | path: '/greetings', 9 | method: 'GET', 10 | handler: () => 11 | Response.json({ 12 | message: `Hello World!`, 13 | }), 14 | }); 15 | const res = await router.fetch('/api/openapi.json'); 16 | const oas = await res.json(); 17 | expect(oas.servers[0].url).toBe('/api'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /patches/jest-leak-detector+29.7.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/jest-leak-detector/build/index.js b/node_modules/jest-leak-detector/build/index.js 2 | index a8ccb1e..70699fd 100644 3 | --- a/node_modules/jest-leak-detector/build/index.js 4 | +++ b/node_modules/jest-leak-detector/build/index.js 5 | @@ -74,26 +74,14 @@ class LeakDetector { 6 | value = null; 7 | } 8 | async isLeaking() { 9 | - this._runGarbageCollector(); 10 | + (0, _v().setFlagsFromString)('--allow-natives-syntax'); 11 | 12 | // wait some ticks to allow GC to run properly, see https://github.com/nodejs/node/issues/34636#issuecomment-669366235 13 | for (let i = 0; i < 10; i++) { 14 | + eval('%CollectGarbage(true)'); 15 | await tick(); 16 | } 17 | return this._isReferenceBeingHeld; 18 | } 19 | - _runGarbageCollector() { 20 | - // @ts-expect-error: not a function on `globalThis` 21 | - const isGarbageCollectorHidden = globalThis.gc == null; 22 | - 23 | - // GC is usually hidden, so we have to expose it before running. 24 | - (0, _v().setFlagsFromString)('--expose-gc'); 25 | - (0, _vm().runInNewContext)('gc')(); 26 | - 27 | - // The GC was not initially exposed, so let's hide it again. 28 | - if (isGarbageCollectorHidden) { 29 | - (0, _v().setFlagsFromString)('--no-expose-gc'); 30 | - } 31 | - } 32 | } 33 | exports.default = LeakDetector; 34 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@theguild/prettier-config'; 2 | 3 | export default { 4 | ...prettierConfig, 5 | plugins: [...prettierConfig.plugins, 'prettier-plugin-tailwindcss'], 6 | }; 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>the-guild-org/shared-config:renovate"], 4 | "automerge": true, 5 | "major": { 6 | "automerge": false 7 | }, 8 | "lockFileMaintenance": { 9 | "enabled": true, 10 | "automerge": true 11 | }, 12 | "packageRules": [ 13 | { 14 | "excludePackagePatterns": [ 15 | "@changesets/*", 16 | "typescript", 17 | "typedoc*", 18 | "^@theguild/", 19 | "@graphql-inspector/core", 20 | "next", 21 | "tailwindcss", 22 | "husky", 23 | "@pulumi/*" 24 | ], 25 | "matchPackagePatterns": ["*"], 26 | "matchUpdateTypes": ["minor", "patch"], 27 | "groupName": "all non-major dependencies", 28 | "groupSlug": "all-minor-patch" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "inlineSourceMap": false, 6 | "incremental": false, 7 | "declaration": true 8 | }, 9 | "exclude": [ 10 | "**/test/*.ts", 11 | "*.spec.ts", 12 | "**/tests", 13 | "**/test-assets", 14 | "**/test-files", 15 | "packages/testing", 16 | "e2e", 17 | "examples", 18 | "**/dist" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "baseUrl": ".", 5 | 6 | "target": "es2021", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "lib": ["esnext", "es2021"], 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "importHelpers": true, 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "incremental": true, 18 | "disableSizeLimit": true, 19 | 20 | "jsx": "preserve", 21 | 22 | "skipLibCheck": true, 23 | 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "noPropertyAccessFromIndexSignature": false, 29 | "paths": { 30 | "@e2e/*": ["e2e/*/src/index.ts"], 31 | "fets": ["packages/fets/src/index.ts"] 32 | } 33 | }, 34 | "include": ["packages", "e2e", "examples"], 35 | "exclude": ["**/node_modules", "**/test-files", "**/dist", "**/e2e", "**/benchmark"] 36 | } 37 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | public/sitemap.xml 3 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | export default { 3 | siteUrl: process.env.SITE_URL || 'https://the-guild.dev/fets', 4 | generateIndexSitemap: false, 5 | exclude: ['*/_meta'], 6 | output: 'export', 7 | }; 8 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | import { withGuildDocs } from '@theguild/components/next.config'; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | export default withGuildDocs({ 5 | output: 'export', 6 | webpack(config) { 7 | config.module.rules.push({ 8 | test: /\.svg$/, 9 | use: ['@svgr/webpack'], 10 | }); 11 | return config; 12 | }, 13 | eslint: { 14 | ignoreDuringBuilds: true, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "next build && next-sitemap --config ./next-sitemap.config.js", 8 | "dev": "next", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@theguild/components": "^7.3.3", 13 | "clsx": "^2.1.1", 14 | "next": "^15.2.2", 15 | "next-sitemap": "^4.2.3", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0", 18 | "react-icons": "^5.0.1" 19 | }, 20 | "devDependencies": { 21 | "@img/sharp-libvips-linux-x64": "1.1.0", 22 | "@svgr/webpack": "^8.0.1", 23 | "@theguild/tailwind-config": "0.6.3", 24 | "@types/node": "22.15.29", 25 | "@types/react": "19.1.6", 26 | "@typescript/sandbox": "^0.1.0", 27 | "monaco-editor": "0.52.2", 28 | "postcss-import": "16.1.0", 29 | "postcss-lightningcss": "1.0.1", 30 | "tailwindcss": "3.4.17", 31 | "typescript": "5.8.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /website/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /website/public/assets/aws-lambda.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/public/assets/azure-functions.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /website/public/assets/fets-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /website/public/assets/fets-text-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/feTS/54f7980727361f0cc1d0cec3c9a65f1c1f0169b6/website/public/assets/fets-text-logo.png -------------------------------------------------------------------------------- /website/public/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /website/public/assets/nextjs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/public/assets/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /website/public/assets/websockets.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/feTS/54f7980727361f0cc1d0cec3c9a65f1c1f0169b6/website/public/favicon.ico -------------------------------------------------------------------------------- /website/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@theguild/components/style.css'; 2 | import { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /website/src/pages/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | title: 'Home', 4 | type: 'page', 5 | display: 'hidden', 6 | theme: { 7 | layout: 'raw', 8 | }, 9 | }, 10 | client: { 11 | title: 'Client', 12 | type: 'page', 13 | }, 14 | server: { 15 | title: 'Server', 16 | type: 'page', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /website/src/pages/client/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'quick-start': 'Quickstart', 3 | 'error-handling': 'Error Handling', 4 | 'request-params': 'Request Parameters', 5 | plugins: 'Plugins', 6 | }; 7 | -------------------------------------------------------------------------------- /website/src/pages/client/client-configuration.mdx: -------------------------------------------------------------------------------- 1 | # Client Configuration 2 | 3 | feTS Client allows you to customize its behavious by enabling various plugins. However, there's also 4 | an additional configuration that can be passed to the `createClient` function. 5 | 6 | ## Customizing The `fetch` Function 7 | 8 | By default, feTS Client uses the `fetch` function provided by the environment. However, you can also 9 | pass a custom `fetch` function to the `createClient` function. 10 | 11 | One possible use case is enabling HTTP/2 support. While HTTP/2 is automatically handled by most 12 | environments, Node.js requires additional configuration to enable it. 13 | 14 | To enable HTTP/2 in Node.js, you can use the `fetch-h2` package: 15 | 16 | ```ts 17 | import fetchH2 from 'fetch-h2' 18 | import { oas } from './oas' 19 | 20 | const client = createClient({ 21 | fetch: fetchH2 as typeof fetch 22 | }) 23 | ``` 24 | 25 | ## Global Parameters 26 | 27 | You can also pass global parameters to the `createClient` function. These parameters will be passed 28 | to every request made by the client. 29 | 30 | ```ts 31 | import { oas } from './oas' 32 | 33 | const client = createClient({ 34 | globalParams: { 35 | headers: { 36 | Authorization: 'Bearer 123' 37 | } 38 | } 39 | }) 40 | ``` 41 | -------------------------------------------------------------------------------- /website/src/pages/client/error-handling.mdx: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | When interacting with APIs, error handling is a crucial aspect. In feTS Client, we return a WHATWG 4 | [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object for every request. This 5 | allows you to either use the `status` attribute to handle specific status codes or the `ok` 6 | attribute to verify whether the response was successful. 7 | 8 | Let's explore how you can manage various scenarios. 9 | 10 | ## Handling Common HTTP Errors 11 | 12 | Suppose you have an endpoint `/pet/{id}` that fetches a pet object. If no pet matches the given id, 13 | the server will return a 404 status code. Here's how you can handle this: 14 | 15 | ```ts 16 | const response = await client['/pet/{id}']({ 17 | params: { 18 | id: 1 19 | } 20 | }) 21 | 22 | if (response.status === 404) { 23 | console.error('Pet not found') 24 | return 25 | } 26 | 27 | // Or handle errors 28 | if (!response.ok) { 29 | console.error('Something went wrong') 30 | const errorResponse = await response.text() 31 | console.log(errorResponse) 32 | return 33 | } 34 | 35 | const pet = await response.json() 36 | console.log(`Pet name is ${pet.name}`) 37 | ``` 38 | 39 | ## Working with Validation Errors in feTS Server 40 | 41 | When using [feTS Server](/server/quick-start), request validation errors are thrown as 42 | `ClientValidationError`. You can handle them as shown below: 43 | 44 | ```ts 45 | import { ClientValidationError } from 'fets' 46 | 47 | try { 48 | const response = await client['/pet/:id']({ 49 | params: { 50 | id: 1 51 | } 52 | }) 53 | 54 | if (response.status === 404) { 55 | console.error('Pet not found') 56 | return 57 | } 58 | 59 | const pet = await response.json() 60 | console.log(`Pet name is ${pet.name}`) 61 | } catch (error) { 62 | if (error instanceof ClientValidationError) { 63 | console.error('Validation error', error.errors) 64 | } 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /website/src/pages/client/inferring-schema-types.mdx: -------------------------------------------------------------------------------- 1 | # Inferring OAS Schema Types 2 | 3 | In feTS, you can use helpers to infer specific fragments of an OpenAPI document. 4 | 5 | ## Models 6 | 7 | To infer models from an OpenAPI document, use the `OASModel` type: 8 | 9 | ```ts filename="user-type.ts" 10 | import type { NormalizeOAS, OASModel } from 'fets' 11 | import type oas from './openapi' 12 | 13 | // This will infer User type from your OpenAPI schema 14 | type User = OASModel, 'User'> 15 | ``` 16 | 17 | ## Request Parameters 18 | 19 | To infer request body parameters from an OpenAPI document, utilize the `OASRequestBody` type: 20 | 21 | ```ts filename="user-request-parameters.ts" 22 | import type { NormalizeOAS, OASRequestParams } from 'fets' 23 | import type oas from './openapi' 24 | 25 | type AddUserInput = OASRequestParams, '/user', 'POST'> 26 | ``` 27 | 28 | ## Response body 29 | 30 | Use the `OASOutput` type, to infer the response body from an OpenAPI document: 31 | 32 | ```ts filename="user-response-body.ts" 33 | import type { NormalizeOAS, OASOutput } from 'fets' 34 | import type oas from './openapi' 35 | 36 | type UserByIdResponse = OASOutput, '/user/:id', 'POST', 200> 37 | ``` 38 | -------------------------------------------------------------------------------- /website/src/pages/client/quick-start.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from '@theguild/components' 2 | 3 | # Quickstart Guide 4 | 5 | Welcome to the feTS Client, a fully type-safe HTTP Client that leverages the 6 | [OpenAPI](https://swagger.io/specification/) specification. It automatically infers types from the 7 | document, providing you with a type-safe interface for seamless API interaction. 8 | 9 | ## Installation 10 | 11 | To install the feTS Client, use the following command: 12 | 13 | ```sh npm2yarn 14 | npm i fets 15 | ``` 16 | 17 | ## Usage with Existing REST API 18 | 19 | 20 | Before diving in, make sure that you have an OpenAPI document, which is a language-agnostic 21 | specification for HTTP APIs. You can usually retrieve it from your server, or manually create it 22 | if you're familiar with the OpenAPI schema. Check out the [OpenAPI 23 | docs](https://swagger.io/specification/) to learn more. 24 | 25 | 26 | Start by creating a TypeScript file that exports your OpenAPI document. Due to certain limitations 27 | in TypeScript, importing types directly from JSON files isn't currently supported 28 | ([see this issue](https://github.com/microsoft/TypeScript/issues/32063)). To work around this, 29 | simply copy and paste the content of your OpenAPI file into the TypeScript file and then export it 30 | with the `as const` modifier. 31 | 32 | ```ts filename="openapi.ts" 33 | export default { openapi: '3.0.0' /* ... */ } as const 34 | ``` 35 | 36 | Next, create a client instance by passing the OpenAPI document to the `createClient` function. 37 | 38 | ```ts 39 | import { createClient, type NormalizeOAS } from 'fets' 40 | import type openapi from './openapi' 41 | 42 | const client = createClient>({}) 43 | 44 | const response = await client['/pets'].get() 45 | 46 | const pets = await response.json() 47 | console.log(pets) 48 | ``` 49 | 50 | 51 | If you get `This node exceeds the maximum length error from TypeScript`, you need to add 52 | `disableSizeLimit: true` to `compilerOptions` in your `tsconfig.json` file. 53 | 54 | 55 | ## Usage with feTS Server 56 | 57 | If you're using feTS Server, and you're sharing the types of your router instance, you can infer 58 | types directly from there. 59 | 60 | ```ts 61 | import { createClient } from 'fets' 62 | // Remember to add `type` to import types only. 63 | import type { router } from './router' 64 | 65 | const client = createClient({ 66 | endpoint: 'http://localhost:3000' 67 | }) 68 | 69 | const response = await client['/pets'].get() 70 | const pets = await response.json() 71 | console.log(pets) 72 | ``` 73 | 74 | --- 75 | 76 | This quick start guide should provide you with a solid foundation for using feTS Client. Enjoy 77 | exploring its full potential! 78 | -------------------------------------------------------------------------------- /website/src/pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | description: 4 | Build and consume REST APIs with ease. No more compromises on type safety in client-server 5 | communication. All thanks to TypeScript and OpenAPI. 6 | --- 7 | 8 | export { IndexPage as default } from '../components/index-page' 9 | -------------------------------------------------------------------------------- /website/src/pages/server/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'quick-start': 'Quickstart', 3 | 'type-safety-and-validation': 'Type-Safety & Validation', 4 | 'error-handling': 'Error Handling', 5 | 'programmatic-schemas': 'Programmatic Schemas with TypeBox', 6 | testing: 'Testing', 7 | cors: 'CORS', 8 | cookies: 'Cookies', 9 | plugins: 'Plugins', 10 | openapi: 'OpenAPI (Swagger)', 11 | comparison: 'Comparison', 12 | integrations: 'Integration & Deployment', 13 | }; 14 | -------------------------------------------------------------------------------- /website/src/pages/server/cookies.mdx: -------------------------------------------------------------------------------- 1 | # Working with Cookies in feTS 2 | 3 | HTTP cookies are small pieces of data used to maintain stateful information between the client and 4 | the server. They allow the server to recognize a client across multiple requests. To learn more 5 | about the concept of cookies, see the 6 | [HTTP Cookies documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). 7 | 8 | In feTS, a plugin is provided for handling cookies in accordance with the web standard 9 | [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore). The plugin allows you 10 | to retrieve cookies from a client's request and send cookies back to the client in your server's 11 | response. Comprehensive details about the CookieStore API are available in 12 | [the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore). 13 | 14 | ## Installation 15 | 16 | ```sh npm2yarn 17 | npm i @whatwg-node/server-plugin-cookies 18 | ``` 19 | 20 | ## How to Use Cookies in feTS 21 | 22 | First, you need to import the necessary modules and create a router with the `useCookies` plugin: 23 | 24 | ```ts filename="cookies.ts" 25 | import { createRouter, Response } from 'fets' 26 | import { useCookies } from '@whatwg-node/server-plugin-cookies' 27 | 28 | const router = createRouter({ 29 | plugins: [useCookies()] 30 | }) 31 | ``` 32 | 33 | Next, you can define routes. In this example, two routes are defined: 34 | 35 | 1. A `GET` route at `/me` that checks if a `session_id` cookie is present, and if so, retrieves the 36 | user associated with that session. If the `session_id` cookie is not found, it responds with a 37 | 401 Unauthorized error. 38 | 39 | ```ts filename="cookies.ts" 40 | .route({ 41 | path: '/me', 42 | method: 'GET', 43 | schemas: {/* ... */}, 44 | handler: async request => { 45 | const sessionId = await request.cookieStore?.get('session_id') 46 | if (!sessionId) { 47 | return Response.json({ error: 'Unauthorized' }, { status: 401 }) 48 | } 49 | const user = await getUserBySessionId(sessionId) 50 | return Response.json(user) 51 | } 52 | }) 53 | ``` 54 | 55 | 2. A `POST` route at `/login` that logs a user in by creating a new session for them and setting the 56 | `session_id` cookie. 57 | 58 | ```ts filename="cookies.ts" 59 | .route({ 60 | path: '/login', 61 | method: 'POST', 62 | handler: async request => { 63 | const { username, password } = await request.json() 64 | const sessionId = await createSessionForUser({ username, password }) 65 | await request.cookieStore?.set('session_id', sessionId) 66 | return Response.json({ message: 'ok' }) 67 | } 68 | }) 69 | ``` 70 | 71 | This is a basic usage of the feTS cookies plugin. Depending on your application, you may need to 72 | implement more complex logic for handling cookies. 73 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | uwebsockets: 'µWebSockets', 3 | 'aws-lambda': 'AWS Lambda', 4 | 'azure-functions': 'Azure Functions', 5 | 'cloudflare-workers': 'Cloudflare Workers', 6 | gcp: 'Google Cloud Functions', 7 | nextjs: 'Next.js', 8 | bun: 'Bun', 9 | deno: 'Deno', 10 | 'node-http': 'Node:HTTP', 11 | fastify: 'Fastify', 12 | }; 13 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/aws-lambda.mdx: -------------------------------------------------------------------------------- 1 | # Integration with AWS Lambda 2 | 3 | AWS Lambda is a serverless computing platform that makes it easy to build applications that run on 4 | the AWS cloud. feTS is platform-agnostic so they can fit together easily. 5 | 6 | ## Installation 7 | 8 | ```sh npm2yarn 9 | npm i fets 10 | ``` 11 | 12 | ## Example 13 | 14 | ```ts filename="fets.ts" 15 | import { createRouter, Response } from 'fets' 16 | import { APIGatewayEvent, APIGatewayProxyResult, Context } from 'aws-lambda' 17 | 18 | const router = createRouter<{ 19 | event: APIGatewayEvent 20 | lambdaContext: Context 21 | }>() 22 | .route({ 23 | method: 'GET', 24 | path: '/greetings', 25 | schemas: { 26 | responses: { 27 | 200: { 28 | type: 'object', 29 | properties: { 30 | message: { 31 | type: 'string' 32 | } 33 | }, 34 | required: ['message'], 35 | additionalProperties: false 36 | } 37 | } 38 | } , 39 | handler: () => Response.json({ message: 'Hello World!' }) 40 | }) 41 | 42 | export async function handler( 43 | event: APIGatewayEvent, 44 | lambdaContext: Context 45 | ): Promise { 46 | const response = await router.fetch( 47 | event.path + '?' + new URLSearchParams(event.queryStringParameters as Record || {}).toString(), 48 | { 49 | { 50 | method: event.httpMethod, 51 | headers: event.headers as HeadersInit, 52 | body: event.body 53 | ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') 54 | : undefined 55 | }, 56 | { 57 | event, 58 | lambdaContext 59 | } 60 | ) 61 | 62 | const responseHeaders: Record = {} 63 | 64 | response.headers.forEach((value, name) => { 65 | responseHeaders[name] = value 66 | }) 67 | 68 | return { 69 | statusCode: response.status, 70 | headers: responseHeaders, 71 | body: await response.text(), 72 | isBase64Encoded: false 73 | } 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/azure-functions.mdx: -------------------------------------------------------------------------------- 1 | # Integration with Azure Functions 2 | 3 | Azure Functions is a serverless environment that supports JavaScript. feTS is platform agnostic and 4 | can be deployed to Azure Functions as well. 5 | 6 | ## Installation 7 | 8 | ```sh npm2yarn 9 | npm i fets @azure/functions 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```ts 15 | import { createRouter, Response } from 'fets' 16 | import { AzureFunction, Context, HttpRequest } from '@azure/functions' 17 | 18 | const router = createRouter().route({ 19 | method: 'GET', 20 | path: '/greetings', 21 | schemas: { 22 | responses: { 23 | 200: { 24 | type: 'object', 25 | properties: { 26 | message: { 27 | type: 'string' 28 | } 29 | }, 30 | required: ['message'], 31 | additionalProperties: false 32 | } 33 | } 34 | }, 35 | handler: () => Response.json({ message: 'Hello World!' }) 36 | }) 37 | 38 | const httpTrigger: AzureFunction = async function ( 39 | context: Context, 40 | req: HttpRequest 41 | ): Promise { 42 | const response = await router.fetch(req.url, { 43 | method: req.method?.toString(), 44 | body: req.rawBody, 45 | headers: req.headers 46 | }) 47 | 48 | const headersObj = {} 49 | response.headers.forEach((value, key) => { 50 | headersObj[key] = value 51 | }) 52 | 53 | const responseText = await response.text() 54 | 55 | context.res = { 56 | status: response.status, 57 | body: responseText, 58 | headers: headersObj 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/bun.mdx: -------------------------------------------------------------------------------- 1 | # Integration with Bun 2 | 3 | feTS provides you a cross-platform HTTP Server. So you can easily integrate it into any platform 4 | besides Node.js. [Bun](https://bun.sh) is a modern JavaScript runtime like Node or Deno, and it 5 | supports Fetch API as a first class citizen. So the configuration is really simple like any other JS 6 | runtime with feTS; 7 | 8 | ## Installation 9 | 10 | ```sh npm2yarn 11 | npm i fets 12 | ``` 13 | 14 | ## Usage 15 | 16 | The following code is a simple example of how to use feTS with Bun. 17 | 18 | ```ts 19 | import { createRouter, Response } from 'fets' 20 | 21 | const router = createRouter().route({ 22 | method: 'GET', 23 | path: '/greetings', 24 | schemas: { 25 | responses: { 26 | 200: { 27 | type: 'object', 28 | properties: { 29 | message: { 30 | type: 'string' 31 | } 32 | }, 33 | required: ['message'], 34 | additionalProperties: false 35 | } 36 | } 37 | }, 38 | handler: () => Response.json({ message: 'Hello World!' }) 39 | }) 40 | 41 | const server = Bun.serve(router) 42 | console.info(`Swagger UI is available at http://localhost:${server.port}/docs`) 43 | ``` 44 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/cloudflare-workers.mdx: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers 2 | 3 | [Cloudflare Workers](https://developers.cloudflare.com/workers) provides a serverless execution 4 | environment that allows you to create entirely new applications or augment existing ones without 5 | configuring or maintaining infrastructure. 6 | 7 | feTS provides you a cross-platform HTTP Server, so it is really easy to integrate feTS into 8 | CloudFlare Workers as well. 9 | 10 | ## Installation 11 | 12 | ```sh npm2yarn 13 | npm i fets 14 | ``` 15 | 16 | ## Example with Service Worker API 17 | 18 | You can use feTS as an event listener for the 19 | [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) in 20 | Cloudflare Workers. 21 | 22 | ```ts 23 | import { createRouter, Response } from 'fets' 24 | 25 | const router = createRouter().route({ 26 | method: 'GET', 27 | path: '/greetings', 28 | schemas: { 29 | responses: { 30 | 200: { 31 | type: 'object', 32 | properties: { 33 | message: { 34 | type: 'string' 35 | } 36 | }, 37 | required: ['message'], 38 | additionalProperties: false 39 | } 40 | } 41 | }, 42 | handler: () => Response.json({ message: 'Hello World!' }) 43 | }) 44 | 45 | self.addEventListener('fetch', router) 46 | ``` 47 | 48 | ## Example with Module Workers API 49 | 50 | You can use feTS with Module Workers. See the difference 51 | [here](https://developers.cloudflare.com/workers/learning/migrating-to-module-workers/). 52 | 53 | ```ts 54 | import { createRouter, Response } from 'fets' 55 | 56 | const router = createRouter().route({ 57 | method: 'GET', 58 | path: '/greetings', 59 | schemas: { 60 | responses: { 61 | 200: { 62 | type: 'object', 63 | properties: { 64 | message: { 65 | type: 'string' 66 | } 67 | }, 68 | required: ['message'], 69 | additionalProperties: false 70 | } 71 | } 72 | }, 73 | handler: () => Response.json({ message: 'Hello World!' }) 74 | }) 75 | 76 | export default { 77 | fetch: router.fetch 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/deno.mdx: -------------------------------------------------------------------------------- 1 | # Integration with Deno 2 | 3 | [Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust](https://deno.land/). 4 | We will use `fets` which has an agnostic HTTP handler using 5 | [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)'s 6 | [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and 7 | [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects. 8 | 9 | ## Example 10 | 11 | Create a `deno-fets.ts` file: 12 | 13 | ```ts filename="deno-fets.ts" 14 | import { createRouter, Response } from 'npm:fets' 15 | 16 | const router = createRouter().route({ 17 | method: 'GET', 18 | path: '/greetings', 19 | schemas: { 20 | responses: { 21 | 200: { 22 | type: 'object', 23 | properties: { 24 | message: { 25 | type: 'string' 26 | } 27 | }, 28 | required: ['message'], 29 | additionalProperties: false 30 | } 31 | } 32 | }, 33 | handler: () => Response.json({ message: 'Hello World!' }) 34 | }) 35 | 36 | Deno.serve(router) 37 | ``` 38 | 39 | And run it: 40 | 41 | ```bash 42 | deno run --allow-net index.ts 43 | ``` 44 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/fastify.mdx: -------------------------------------------------------------------------------- 1 | # Integration with Fastify 2 | 3 | [Fastify is one of the popular HTTP server frameworks for Node.js.](https://www.fastify.io/). 4 | 5 | ## Installation 6 | 7 | ```sh npm2yarn 8 | npm i fets fastify 9 | ``` 10 | 11 | ## Example 12 | 13 | ```ts 14 | import fastify, { FastifyReply, FastifyRequest } from 'fastify' 15 | import { createRouter, Response } from 'fets' 16 | 17 | const app = fastify() 18 | 19 | const router = createRouter().route({ 20 | method: 'GET', 21 | path: '/greetings', 22 | schemas: { 23 | responses: { 24 | 200: { 25 | type: 'object', 26 | properties: { 27 | message: { 28 | type: 'string' 29 | } 30 | }, 31 | required: ['message'], 32 | additionalProperties: false 33 | } 34 | } 35 | }, 36 | handler: () => Response.json({ message: 'Hello World!' }) 37 | }) 38 | 39 | app.route({ 40 | method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 41 | url: '/*', 42 | async handler(req, reply) { 43 | const response = await router.handleNodeRequest(req, { 44 | req, 45 | reply 46 | }) 47 | 48 | if (response === undefined) { 49 | void reply.status(404).send('Not found.') 50 | return reply 51 | } 52 | 53 | response.headers.forEach((value, key) => { 54 | void reply.header(key, value) 55 | }) 56 | 57 | void reply.status(response.status) 58 | 59 | void reply.send(response.body) 60 | } 61 | }) 62 | 63 | app.listen(4000) 64 | ``` 65 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/gcp.mdx: -------------------------------------------------------------------------------- 1 | # Google Cloud Functions 2 | 3 | [Google Cloud Functions](https://cloud.google.com/functions/) is a hosted serverless platform 4 | solution provided by Google. It integrates with feTS very well thanks to feTS's platform independent 5 | API. 6 | 7 | ## Installation 8 | 9 | ```sh npm2yarn 10 | npm i fets 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```ts 16 | import { createRouter, Response } from 'fets' 17 | 18 | export const api = createRouter().route({ 19 | method: 'GET', 20 | path: '/greetings', 21 | schemas: { 22 | responses: { 23 | 200: { 24 | type: 'object', 25 | properties: { 26 | message: { 27 | type: 'string' 28 | } 29 | }, 30 | required: ['message'], 31 | additionalProperties: false 32 | } 33 | } 34 | }, 35 | handler: () => Response.json({ message: 'Hello World!' }) 36 | }) 37 | ``` 38 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/nextjs.mdx: -------------------------------------------------------------------------------- 1 | # Integration with Next.js 2 | 3 | [Next.js](https://nextjs.org) is a web framework that allows you to build websites very quickly and 4 | feTS can be integrated with Next.js easily as 5 | [an API middleware](https://nextjs.org/docs/api-routes/api-middlewares). 6 | 7 | You can also consume the router in type-safe way by inferring router types from the server side. 8 | 9 | ## Installation 10 | 11 | ```sh npm2yarn 12 | npm i fets 13 | ``` 14 | 15 | ## Usage 16 | 17 | We recommend to use feTS as a main middleware for your API routes. In this case, you should create a 18 | `route.ts` file under `app/api/[...slug]` directory. Since your base route is `/api`, you should 19 | configure `base`. Then you can create a router and export it as `GET` and `POST` methods. 20 | 21 | ```ts filename=app/api/[...slug]/route.ts 22 | import { createRouter, Response, Type } from 'fets' 23 | 24 | export const router = createRouter({ 25 | base: '/api' 26 | }).route({ 27 | method: 'GET', 28 | path: '/greetings', 29 | schemas: { 30 | responses: { 31 | 200: Type.Object({ 32 | message: Type.String() 33 | }) 34 | } 35 | }, 36 | handler: () => Response.json({ message: 'Hello World!' }) 37 | }) 38 | 39 | export { router as GET, router as POST } 40 | ``` 41 | 42 | ## Type-safe client usage 43 | 44 | Then on the client side, you can use the type-safe client. You should import the router from the 45 | server side as a `type`, then you can create a client with the router type with `endpoint` option 46 | set to `/api`. 47 | 48 | ```tsx filename = pages/index.tsx 49 | import { createClient } from 'fets' 50 | import type router from './api/[...slug]/router' 51 | 52 | const client = createClient({ 53 | endpoint: '/api' 54 | }) 55 | 56 | export default function Home({ greetingsResponse }: Props) { 57 | const [message, setMessage] = useState('') 58 | useEffect(() => { 59 | client['/greetings'] 60 | .get() 61 | .then(res => res.json()) 62 | .then(res => setMessage(res.message)) 63 | .catch(err => setMessage(`Error: ${err.message}`)) 64 | }, []) 65 | return ( 66 |
67 |

Greetings Message from API: {greetingsResponse.message}

68 |
69 | ) 70 | } 71 | ``` 72 | 73 | > You can see the full example 74 | > [here](https://github.com/ardatan/fets/tree/master/examples/nextjs-example). 75 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/node-http.mdx: -------------------------------------------------------------------------------- 1 | # Node HTTP 2 | 3 | Node.js is the most popular JavaScript runtime environment, and feTS supports it fully as well with 4 | a few lines of code. 5 | 6 | This recipe shows how to use feTS with Node.js' built-in HTTP implementation. 7 | 8 | ## Installation 9 | 10 | ```sh npm2yarn 11 | npm i fets 12 | ``` 13 | 14 | ## Example 15 | 16 | ```ts 17 | import { createServer } from 'node:http' 18 | import { createRouter, Response } from 'fets' 19 | 20 | const router = createRouter().route({ 21 | method: 'GET', 22 | path: '/greetings', 23 | schemas: { 24 | responses: { 25 | 200: { 26 | type: 'object', 27 | properties: { 28 | hello: { type: 'string' } 29 | } 30 | } 31 | } 32 | }, 33 | handler: () => Response.json({ hello: 'world' }) 34 | }) 35 | 36 | createServer(router).listen(3000, () => { 37 | console.log('Swagger UI is available at http://localhost:3000/docs') 38 | }) 39 | ``` 40 | -------------------------------------------------------------------------------- /website/src/pages/server/integrations/uwebsockets.mdx: -------------------------------------------------------------------------------- 1 | # Integration with µWebSockets 2 | 3 | [µWebSockets.js is an HTTP/WebSocket server for Node.js.](https://github.com/uNetworking/uWebSockets.js/) 4 | 5 | ## Installation 6 | 7 | ```sh npm2yarn 8 | npm i fets uWebSockets.js@uNetworking/uWebSockets.js#semver:^20 9 | ``` 10 | 11 | ## Example 12 | 13 | ```ts filename="index.ts" 14 | import { createRouter, Response } from 'fets' 15 | import { App, HttpRequest, HttpResponse } from 'uWebSockets.js' 16 | 17 | interface ServerContext { 18 | req: HttpRequest 19 | res: HttpResponse 20 | } 21 | 22 | const router = createRouter().route({ 23 | method: 'GET', 24 | path: '/greetings', 25 | schemas: { 26 | responses: { 27 | 200: { 28 | type: 'object', 29 | properties: { 30 | message: { 31 | type: 'string' 32 | } 33 | }, 34 | required: ['message'], 35 | additionalProperties: false 36 | } 37 | } 38 | }, 39 | handler: () => Response.json({ message: 'Hello World!' }) 40 | }) 41 | 42 | App() 43 | .any('/*', router) 44 | .listen(3000, () => { 45 | console.log(`Swagger UI is running on http://localhost:3000/docs`) 46 | }) 47 | ``` 48 | -------------------------------------------------------------------------------- /website/src/pages/server/plugins.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from '@theguild/components' 2 | 3 | # Plugins 4 | 5 | The feTS Server includes an extension system, offering the ability to interact with the 6 | request/response process. 7 | 8 | - `onRequest` - Invoked prior to the router handling the request. 9 | - It features an `endResponse` function that takes a `Response` object to preemptively conclude 10 | the request. 11 | - `onResponse` - Invoked following the router's handling of the request. 12 | - It offers the ability to alter the response before it's delivered to the client. 13 | - `onRouteHandle` - When a route is matched, this is invoked with all the details of the route. So 14 | you can apply any logic (tracing, auth etc) before the actual handler is invoked. 15 | 16 | ## Recording the Response Delay 17 | 18 | We'll develop an extension that determines the response delay and logs it in the console. 19 | 20 | ```ts 21 | import { RouterPlugin } from 'fets' 22 | 23 | export function useLogDelay(): RouterPlugin { 24 | const initialTimePerRequest = new WeakMap() 25 | return { 26 | onRequest({ request }) { 27 | initialTimePerRequest.set(request, Date.now()) 28 | }, 29 | onResponse({ request, response }) { 30 | const initialTime = initialTimePerRequest.get(request) 31 | if (initialTime) { 32 | const delay = Date.now() - initialTime 33 | console.log(`Response delay: ${delay}ms`) 34 | } 35 | } 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /website/src/pages/server/programmatic-schemas.mdx: -------------------------------------------------------------------------------- 1 | # Building Schemas Programmatically with TypeBox 2 | 3 | Instead of using JSON Schemas, you have the option to define your schemas using 4 | [TypeBox](https://github.com/sinclairzx81/typebox). 5 | 6 | ## Example 7 | 8 | ```typescript 9 | import { createRouter, Response, Type } from 'fets' 10 | 11 | const router = createRouter().route({ 12 | path: '/user/:id', 13 | method: 'POST', 14 | schemas: { 15 | request: { 16 | headers: Type.Object({ 17 | authorization: Type.String().regex(/Bearer .+/) 18 | }), 19 | params: Type.Object({ 20 | id: Type.String({ 21 | format: 'uuid' 22 | }) 23 | }) 24 | } 25 | }, 26 | // Response type will be automatically inferred from the handler's return type 27 | handler: request => { 28 | if (request.params.id !== EXPECTED_UUID) { 29 | return Response.json( 30 | { 31 | message: 'User not found' 32 | }, 33 | { 34 | status: 404 35 | } 36 | ) 37 | } 38 | return Response.json({ 39 | id: request.params.id, 40 | name: 'John Doe' 41 | }) 42 | } 43 | }) 44 | ``` 45 | 46 | ## Why not Zod? 47 | 48 | feTS uses JSON Schemas, and libraries like Zod have their own API and logic to handle validations 49 | and modeling. With feTS, you can use any tool that generates JSON Schemas programmatically but we 50 | recommend using TypeBox which keeps the types of output JSON Schemas so feTS can infer types from 51 | them. And without any conversion process, those generated schemas can be directly added into the 52 | OpenAPI document. 53 | 54 | In addition, you can see how fast TypeBox is; 55 | [Type Benchmarks](https://moltar.github.io/typescript-runtime-type-benchmarks/) 56 | -------------------------------------------------------------------------------- /website/src/pages/server/testing.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from '@theguild/components' 2 | 3 | # Testing 4 | 5 | feTS natively supports HTTP injection, offering compatibility with any testing framework you prefer. 6 | 7 | The `fetch` function on your router instance can be used to simulate an HTTP request. 8 | 9 | 10 | Using the router.fetch function doesn't trigger a real HTTP request. Instead, it emulates the HTTP 11 | request in a manner that is fully compatible with the way Request/Response operate. 12 | 13 | 14 | ## Using the `fetch` Function 15 | 16 | The `fetch` function can be directly employed from the router instance, or it can be given to any 17 | client that accepts a `fetch` function. 18 | 19 | The `router.fetch` is compliant with the 20 | [WHATWG `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. 21 | 22 | ```ts 23 | import { router } from './router' 24 | 25 | const response = await router.fetch('/todo', { 26 | method: 'PUT', 27 | headers: { 28 | 'Content-Type': 'application/json' 29 | }, 30 | body: JSON.stringify({ 31 | title: 'Buy milk', 32 | completed: false 33 | }) 34 | }) 35 | 36 | const responseJson = await response.json() 37 | console.assert(responseJson.title === 'Buy milk', 'Title should be "Buy milk"') 38 | ``` 39 | 40 | ### Creating a feTS Client for Testing 41 | 42 | You can use [feTS Client](/client/quick-start) to test your API in a type-safe way. It doesn't 43 | require you to launch any actual HTTP server. 44 | 45 | ```ts 46 | import { createClient } from 'fets' 47 | import { router } from './router' 48 | 49 | const client = createClient({ 50 | fetchFn: router.fetch, 51 | endpoint: 'http://localhost:3000' 52 | }) 53 | 54 | // Everything below is fully typed 55 | const response = await client['/todo'].put({ 56 | json: { 57 | title: 'Buy milk', 58 | completed: false 59 | } 60 | }) 61 | 62 | const responseJson = await response.json() 63 | console.assert(responseJson.title === 'Buy milk', 'Title should be "Buy milk"') 64 | ``` 65 | -------------------------------------------------------------------------------- /website/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindConfig, { Config } from '@theguild/tailwind-config'; 2 | 3 | export default { 4 | ...tailwindConfig, 5 | theme: { 6 | ...tailwindConfig.theme, 7 | extend: { 8 | colors: { 9 | ...tailwindConfig.theme.extend.colors, 10 | dark: '#0b0d11', 11 | // gray name conflicts with @theguild/components 12 | secondary: { 13 | 100: '#f3f4f6', 14 | 200: '#d0d3da', 15 | 300: '#70788a', 16 | 400: '#4e5665', 17 | 500: '#394150', 18 | 600: '#1c212c', 19 | }, 20 | primary: '#1886ff', 21 | }, 22 | }, 23 | }, 24 | } satisfies Config; 25 | -------------------------------------------------------------------------------- /website/theme.config.tsx: -------------------------------------------------------------------------------- 1 | /* eslint sort-keys: error */ 2 | import { useRouter } from 'next/router'; 3 | import { defineConfig, Giscus, PRODUCTS, useTheme } from '@theguild/components'; 4 | 5 | export default defineConfig({ 6 | description: PRODUCTS.FETS.title, 7 | docsRepositoryBase: 'https://github.com/ardatan/fets/tree/master/website', 8 | logo: PRODUCTS.FETS.logo, 9 | main({ children }) { 10 | const { resolvedTheme } = useTheme(); 11 | const { route } = useRouter(); 12 | 13 | const comments = route !== '/' && ( 14 | 24 | ); 25 | return ( 26 | <> 27 | {children} 28 | {comments} 29 | 30 | ); 31 | }, 32 | websiteName: 'FETS', 33 | }); 34 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["dom", "dom.iterable", "esnext"], 10 | "allowJs": true, 11 | "noEmit": true, 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------