├── .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 | 
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 |
11 |
--------------------------------------------------------------------------------
/examples/nextjs-example/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/examples/nextjs-example/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
14 |
--------------------------------------------------------------------------------
/website/public/assets/azure-functions.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/website/public/assets/fets-logo.svg:
--------------------------------------------------------------------------------
1 |
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 |
9 |
--------------------------------------------------------------------------------
/website/public/assets/nextjs.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/website/public/assets/typescript.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/website/public/assets/websockets.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------