├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nxreleaserc.json ├── .prettierignore ├── .prettierrc ├── .secretlintrc.json ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── apps ├── .gitkeep ├── demo-express-app │ ├── .env.example │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js ├── demo-nextjs-app-router │ ├── .env.example │ ├── .eslintrc.json │ ├── app │ │ ├── api │ │ │ └── fal │ │ │ │ └── proxy │ │ │ │ └── route.ts │ │ ├── camera-turbo │ │ │ └── page.tsx │ │ ├── comfy │ │ │ ├── image-to-image │ │ │ │ └── page.tsx │ │ │ ├── image-to-video │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── text-to-image │ │ │ │ └── page.tsx │ │ ├── global.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── queue │ │ │ └── page.tsx │ │ ├── realtime │ │ │ └── page.tsx │ │ ├── streaming │ │ │ └── page.tsx │ │ └── whisper │ │ │ └── page.tsx │ ├── components │ │ ├── drawing.tsx │ │ └── drawingState.json │ ├── index.d.ts │ ├── jest.config.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── postcss.config.js │ ├── project.json │ ├── public │ │ ├── .gitkeep │ │ └── favicon.ico │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsconfig.spec.json └── demo-nextjs-page-router │ ├── .env.example │ ├── .eslintrc.json │ ├── index.d.ts │ ├── jest.config.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── fal │ │ │ └── proxy.ts │ ├── index.module.css │ ├── index.tsx │ └── styles.css │ ├── postcss.config.js │ ├── project.json │ ├── public │ ├── .gitkeep │ └── placeholder@2x.jpg │ ├── specs │ └── index.spec.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsconfig.spec.json ├── babel.config.json ├── cspell-dictionary.txt ├── cspell.json ├── docs └── reference │ ├── .nojekyll │ ├── assets │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── navigation.js │ ├── search.js │ ├── style.css │ └── typedoc-github-style.css │ └── index.html ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep ├── client │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── auth.ts │ │ ├── client.spec.ts │ │ ├── client.ts │ │ ├── config.spec.ts │ │ ├── config.ts │ │ ├── index.ts │ │ ├── middleware.ts │ │ ├── queue.ts │ │ ├── realtime.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── retry.spec.ts │ │ ├── retry.ts │ │ ├── runtime.spec.ts │ │ ├── runtime.ts │ │ ├── storage.ts │ │ ├── streaming.ts │ │ ├── types │ │ │ ├── client.ts │ │ │ ├── common.ts │ │ │ └── endpoints.ts │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── create-app │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── proxy │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── express.ts │ ├── hono.ts │ ├── index.ts │ ├── nextjs.ts │ ├── remix.ts │ └── svelte.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package-lock.json ├── package.json ├── tools └── tsconfig.tools.json ├── tsconfig.base.json └── typedoc.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.json"], 8 | "parser": "jsonc-eslint-parser", 9 | "rules": { 10 | "@nx/dependency-checks": "error" 11 | } 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 15 | "rules": { 16 | "@nx/enforce-module-boundaries": [ 17 | "error", 18 | { 19 | "enforceBuildableLibDependency": true, 20 | "allow": [], 21 | "depConstraints": [ 22 | { 23 | "sourceTag": "*", 24 | "onlyDependOnLibsWithTags": ["*"] 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | }, 31 | { 32 | "files": ["*.ts", "*.tsx"], 33 | "extends": ["plugin:@nx/typescript"], 34 | "rules": {} 35 | }, 36 | { 37 | "files": ["*.js", "*.jsx"], 38 | "extends": ["plugin:@nx/javascript"], 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json linguist-generated 2 | docs/reference/** linguist-generated 3 | libs/client/src/types/endpoints.ts linguist-generated 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repo 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: "18.x" 20 | cache: "npm" 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Format check 24 | run: npx nx affected --target=format --base=remotes/origin/${{ github.base_ref }} 25 | - name: Lint 26 | run: npx nx affected --target=lint --base=remotes/origin/${{ github.base_ref }} 27 | - name: Test 28 | run: npx nx affected --target=test --base=remotes/origin/${{ github.base_ref }} 29 | - name: Build 30 | run: npx nx affected --target=build --prod --base=remotes/origin/${{ github.base_ref }} 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*-v[0-9]+.[0-9]+.[0-9]+*" 7 | workflow_dispatch: 8 | inputs: 9 | package: 10 | description: "Package to release" 11 | required: true 12 | type: choice 13 | options: 14 | - client 15 | - proxy 16 | version: 17 | description: "Version to release (e.g., 1.2.0, 1.3.0-alpha.0)" 18 | required: true 19 | type: string 20 | dry_run: 21 | description: "Dry run (will not publish to npm)" 22 | required: false 23 | type: boolean 24 | default: true 25 | 26 | jobs: 27 | build-and-publish: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: "20" 39 | registry-url: "https://registry.npmjs.org" 40 | 41 | - name: Install dependencies 42 | run: npm ci 43 | 44 | - name: Extract package info 45 | id: tag-info 46 | run: | 47 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 48 | # Manual trigger 49 | PACKAGE_NAME="${{ inputs.package }}" 50 | VERSION="${{ inputs.version }}" 51 | else 52 | # Tag trigger 53 | TAG_NAME=${{ github.ref_name }} 54 | PACKAGE_NAME=$(echo $TAG_NAME | cut -d'-' -f1) 55 | VERSION=$(echo $TAG_NAME | cut -d'-' -f2- | sed 's/^v//') 56 | fi 57 | 58 | # Extract npm tag based on version qualifier 59 | if [[ $VERSION =~ .*-([a-zA-Z]+)\.[0-9]+$ ]]; then 60 | NPM_TAG="${BASH_REMATCH[1]}" 61 | else 62 | NPM_TAG="latest" 63 | fi 64 | 65 | echo "package=$PACKAGE_NAME" >> $GITHUB_OUTPUT 66 | echo "version=$VERSION" >> $GITHUB_OUTPUT 67 | echo "npm_tag=$NPM_TAG" >> $GITHUB_OUTPUT 68 | 69 | - name: Build package 70 | run: npx nx build ${{ steps.tag-info.outputs.package }} --prod 71 | 72 | - name: Verify package version 73 | run: | 74 | PACKAGE_JSON_VERSION=$(node -p "require('./dist/libs/${{ steps.tag-info.outputs.package }}/package.json').version") 75 | if [ "$PACKAGE_JSON_VERSION" != "${{ steps.tag-info.outputs.version }}" ]; then 76 | echo "Package version ($PACKAGE_JSON_VERSION) does not match tag version (${{ steps.tag-info.outputs.version }})" 77 | exit 1 78 | fi 79 | 80 | - name: Publish package 81 | run: | 82 | echo "Publishing ${{ steps.tag-info.outputs.package }}@${{ steps.tag-info.outputs.version }} with tag ${{ steps.tag-info.outputs.npm_tag }}" 83 | cd dist/libs/${{ steps.tag-info.outputs.package }} 84 | npm publish --access public --tag ${{ steps.tag-info.outputs.npm_tag }} ${{ inputs.dry_run && '--dry-run' || '' }} 85 | env: 86 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Next.js 42 | .next 43 | *.local 44 | .vercel 45 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | -------------------------------------------------------------------------------- /.nxreleaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelog": true, 3 | "npm": true, 4 | "github": true, 5 | "repositoryUrl": "https://github.com/fal-ai/fal-js", 6 | "branches": ["main"] 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | /docs 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "plugins": ["prettier-plugin-organize-imports"] 5 | } 6 | -------------------------------------------------------------------------------- /.secretlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "id": "@secretlint/secretlint-rule-preset-recommend" 5 | }, 6 | { 7 | "id": "@secretlint/secretlint-rule-pattern", 8 | "options": { 9 | "patterns": [ 10 | { 11 | "name": "Fal API key", 12 | "pattern": "/\\b[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89abAB][0-9a-f]{3}-[0-9a-f]{12}:[0-9a-fA-F]{32}\\b/" 13 | } 14 | ] 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 https://fal.ai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The fal.ai JS client 2 | 3 | ![@fal-ai/client npm package](https://img.shields.io/npm/v/@fal-ai/client?color=%237527D7&label=client&style=flat-square) 4 | ![@fal-ai/server-proxy npm package](https://img.shields.io/npm/v/@fal-ai/server-proxy?color=%237527D7&label=proxy&style=flat-square) 5 | ![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/fal-js/build.yml?style=flat-square) 6 | ![License](https://img.shields.io/github/license/fal-ai/fal-js?style=flat-square) 7 | 8 | ## About the Project 9 | 10 | The fal JavaScript/TypeScript Client is a robust and user-friendly library designed for seamless integration of fal endpoints in Web, Node.js, and React Native applications. Developed in TypeScript, it provides developers with type safety right from the start. 11 | 12 | ## Getting Started 13 | 14 | The `@fal-ai/client` library serves as a client for fal apps hosted on fal. For guidance on consuming and creating apps, refer to the [quickstart guide](https://fal.ai/docs). 15 | 16 | ### Client Library 17 | 18 | This client library is crafted as a lightweight layer atop platform standards like `fetch`. This ensures a hassle-free integration into your existing codebase. Moreover, it addresses platform disparities, guaranteeing flawless operation across various JavaScript runtimes. 19 | 20 | > **Note:** 21 | > Ensure you've reviewed the [getting started guide](https://fal.ai/docs) to acquire your credentials, browser existing APIs, or create your custom functions. 22 | 23 | 1. Install the client library 24 | ```sh 25 | npm install --save @fal-ai/client 26 | ``` 27 | 2. Start by configuring your credentials: 28 | 29 | ```ts 30 | import { fal } from "@fal-ai/client"; 31 | 32 | fal.config({ 33 | // Can also be auto-configured using environment variables: 34 | credentials: "FAL_KEY", 35 | }); 36 | ``` 37 | 38 | 3. Retrieve your function id and execute it: 39 | ```ts 40 | const result = await fal.run("user/app-alias"); 41 | ``` 42 | 43 | The result's type is contingent upon your Python function's output. Types in Python are mapped to their corresponding types in JavaScript. 44 | 45 | See the available [model APIs](https://fal.ai/models) for more details. 46 | 47 | ### The fal client proxy 48 | 49 | Although the fal client is designed to work in any JS environment, including directly in your browser, **it is not recommended** to store your credentials in your client source code. The common practice is to use your own server to serve as a proxy to fal APIs. Luckily fal supports that out-of-the-box with plug-and-play proxy functions for the most common engines/frameworks. 50 | 51 | For example, if you are using Next.js, you can: 52 | 53 | 1. Instal the proxy library 54 | ```sh 55 | npm install --save @fal-ai/server-proxy 56 | ``` 57 | 2. Add the proxy as an API endpoint of your app, see an example here in [pages/api/fal/proxy.ts](https://github.com/fal-ai/fal-js/blob/main/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts) 58 | ```ts 59 | export { handler as default } from "@fal-ai/server-proxy/nextjs"; 60 | ``` 61 | 3. Configure the client to use the proxy: 62 | ```ts 63 | import { fal } from "@fal-ai/client"; 64 | fal.config({ 65 | proxyUrl: "/api/fal/proxy", 66 | }); 67 | ``` 68 | 4. Make sure your server has `FAL_KEY` as environment variable with a valid API Key. That's it! Now your client calls will route through your server proxy, so your credentials are protected. 69 | 70 | See [libs/proxy](./libs/proxy/) for more details. 71 | 72 | ### The example Next.js app 73 | 74 | You can find a minimal Next.js + fal application examples in [apps/demo-nextjs-page-router/](https://github.com/fal-ai/fal-js/blob/main/apps/demo-nextjs-page-router). 75 | 76 | 1. Run `npm install` on the repository root. 77 | 2. Create a `.env.local` file and add your API Key as `FAL_KEY` environment variable (or export it any other way your prefer). 78 | 3. Run `npx nx serve demo-nextjs-page-router` to start the Next.js app (`demo-nextjs-app-router` is also available if you're interested in the app router version). 79 | 80 | Check our [Next.js integration docs](https://fal.ai/docs/integrations/nextjs) for more details. 81 | 82 | ## Roadmap 83 | 84 | See the [open feature requests](https://github.com/fal-ai/fal-js/labels/enhancement) for a list of proposed features and join the discussion. 85 | 86 | ## Contributing 87 | 88 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 89 | 90 | 1. Make sure you read our [Code of Conduct](https://github.com/fal-ai/fal-js/blob/main/CODE_OF_CONDUCT.md) 91 | 2. Fork the project and clone your fork 92 | 3. Setup the local environment with `npm install` 93 | 4. Create a feature branch (`git checkout -b feature/add-cool-thing`) or a bugfix branch (`git checkout -b fix/smash-that-bug`) 94 | 5. Commit the changes (`git commit -m 'feat(client): added a cool thing'`) - use [conventional commits](https://conventionalcommits.org) 95 | 6. Push to the branch (`git push --set-upstream origin feature/add-cool-thing`) 96 | 7. Open a Pull Request 97 | 98 | Check the [good first issue queue](https://github.com/fal-ai/fal-js/labels/good+first+issue), your contribution will be welcome! 99 | 100 | ## License 101 | 102 | Distributed under the MIT License. See [LICENSE](https://github.com/fal-ai/fal-js/blob/main/LICENSE) for more information. 103 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/.gitkeep -------------------------------------------------------------------------------- /apps/demo-express-app/.env.example: -------------------------------------------------------------------------------- 1 | # Rename this file to .env.local and add your fal credentials 2 | # Visit https://fal.ai to get started 3 | FAL_KEY="FAL_KEY_ID:FAL_KEY_SECRET" 4 | -------------------------------------------------------------------------------- /apps/demo-express-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/demo-express-app/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "demo-express-app", 4 | preset: "../../jest.preset.js", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], 8 | }, 9 | moduleFileExtensions: ["ts", "js", "html"], 10 | coverageDirectory: "../../coverage/apps/demo-express-app", 11 | }; 12 | -------------------------------------------------------------------------------- /apps/demo-express-app/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-express-app", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/demo-express-app/src", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/webpack:webpack", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "target": "node", 13 | "compiler": "tsc", 14 | "outputPath": "dist/apps/demo-express-app", 15 | "main": "apps/demo-express-app/src/main.ts", 16 | "tsConfig": "apps/demo-express-app/tsconfig.app.json", 17 | "assets": ["apps/demo-express-app/src/assets"], 18 | "isolatedConfig": true, 19 | "webpackConfig": "apps/demo-express-app/webpack.config.js" 20 | }, 21 | "configurations": { 22 | "development": {}, 23 | "production": {} 24 | } 25 | }, 26 | "serve": { 27 | "executor": "@nx/js:node", 28 | "defaultConfiguration": "development", 29 | "options": { 30 | "buildTarget": "demo-express-app:build" 31 | }, 32 | "configurations": { 33 | "development": { 34 | "buildTarget": "demo-express-app:build:development" 35 | }, 36 | "production": { 37 | "buildTarget": "demo-express-app:build:production" 38 | } 39 | } 40 | }, 41 | "lint": { 42 | "executor": "@nx/linter:eslint", 43 | "outputs": ["{options.outputFile}"], 44 | "options": { 45 | "lintFilePatterns": ["apps/demo-express-app/**/*.ts"] 46 | } 47 | }, 48 | "test": { 49 | "executor": "@nx/jest:jest", 50 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 51 | "options": { 52 | "jestConfig": "apps/demo-express-app/jest.config.ts", 53 | "passWithNoTests": true 54 | }, 55 | "configurations": { 56 | "ci": { 57 | "ci": true, 58 | "codeCoverage": true 59 | } 60 | } 61 | } 62 | }, 63 | "tags": [] 64 | } 65 | -------------------------------------------------------------------------------- /apps/demo-express-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/demo-express-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/demo-express-app/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import { fal } from "@fal-ai/client"; 7 | import * as falProxy from "@fal-ai/server-proxy/express"; 8 | import cors from "cors"; 9 | import { configDotenv } from "dotenv"; 10 | import express from "express"; 11 | import * as path from "path"; 12 | 13 | configDotenv({ path: "./env.local" }); 14 | 15 | const app = express(); 16 | 17 | // Middlewares 18 | app.use("/assets", express.static(path.join(__dirname, "assets"))); 19 | app.use(express.json()); 20 | 21 | // fal.ai client proxy 22 | app.all(falProxy.route, cors(), falProxy.handler); 23 | 24 | // Your API endpoints 25 | app.get("/api", (req, res) => { 26 | res.send({ message: "Welcome to demo-express-app!" }); 27 | }); 28 | 29 | app.get("/fal-on-server", async (req, res) => { 30 | const result = await fal.run("110602490-lcm", { 31 | input: { 32 | prompt: 33 | "a black cat with glowing eyes, cute, adorable, disney, pixar, highly detailed, 8k", 34 | }, 35 | }); 36 | res.send(result); 37 | }); 38 | 39 | const port = process.env.PORT || 3333; 40 | const server = app.listen(port, () => { 41 | console.log(`Listening at http://localhost:${port}/api`); 42 | }); 43 | server.on("error", console.error); 44 | -------------------------------------------------------------------------------- /apps/demo-express-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node", "express"] 7 | }, 8 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 9 | "include": ["src/**/*.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demo-express-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "esModuleInterop": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/demo-express-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/demo-express-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { composePlugins, withNx } = require("@nx/webpack"); 2 | 3 | // Nx plugins for webpack. 4 | module.exports = composePlugins(withNx(), (config) => { 5 | // Update the webpack config as needed here. 6 | // e.g. `config.plugins.push(new MyPlugin())` 7 | return config; 8 | }); 9 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/.env.example: -------------------------------------------------------------------------------- 1 | # Rename this file to .env.local and add your fal credentials 2 | # Visit https://fal.ai to get started 3 | FAL_KEY="FAL_KEY_ID:FAL_KEY_SECRET" 4 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": ["!**/*", ".next/**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.*"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": "off" 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 18 | "rules": { 19 | "@next/next/no-html-link-for-pages": [ 20 | "error", 21 | "apps/demo-nextjs-app-router/pages" 22 | ] 23 | } 24 | }, 25 | { 26 | "files": ["*.ts", "*.tsx"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "rules": {} 32 | }, 33 | { 34 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 35 | "env": { 36 | "jest": true 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts: -------------------------------------------------------------------------------- 1 | import { route } from "@fal-ai/server-proxy/nextjs"; 2 | 3 | export const { GET, POST, PUT } = route; 4 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/camera-turbo/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import { createFalClient } from "@fal-ai/client"; 5 | import { MutableRefObject, useEffect, useRef, useState } from "react"; 6 | 7 | const fal = createFalClient({ 8 | proxyUrl: "/api/fal/proxy", 9 | }); 10 | 11 | const EMPTY_IMG = 12 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjOHPmzH8ACDADZKt3GNsAAAAASUVORK5CYII="; 13 | 14 | type WebcamOptions = { 15 | videoRef: MutableRefObject; 16 | previewRef: MutableRefObject; 17 | onFrameUpdate?: (data: Uint8Array) => void; 18 | width?: number; 19 | height?: number; 20 | }; 21 | const useWebcam = ({ 22 | videoRef, 23 | previewRef, 24 | onFrameUpdate, 25 | width = 512, 26 | height = 512, 27 | }: WebcamOptions) => { 28 | useEffect(() => { 29 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 30 | navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => { 31 | if (videoRef.current !== null) { 32 | videoRef.current.srcObject = stream; 33 | videoRef.current.play(); 34 | } 35 | }); 36 | } 37 | }, [videoRef]); 38 | 39 | const captureFrame = () => { 40 | const canvas = previewRef.current; 41 | const video = videoRef.current; 42 | if (canvas === null || video === null) { 43 | return; 44 | } 45 | 46 | // Calculate the aspect ratio and crop dimensions 47 | const aspectRatio = video.videoWidth / video.videoHeight; 48 | let sourceX, sourceY, sourceWidth, sourceHeight; 49 | 50 | if (aspectRatio > 1) { 51 | // If width is greater than height 52 | sourceWidth = video.videoHeight; 53 | sourceHeight = video.videoHeight; 54 | sourceX = (video.videoWidth - video.videoHeight) / 2; 55 | sourceY = 0; 56 | } else { 57 | // If height is greater than or equal to width 58 | sourceWidth = video.videoWidth; 59 | sourceHeight = video.videoWidth; 60 | sourceX = 0; 61 | sourceY = (video.videoHeight - video.videoWidth) / 2; 62 | } 63 | 64 | // Resize the canvas to the target dimensions 65 | canvas.width = width; 66 | canvas.height = height; 67 | 68 | const context = canvas.getContext("2d"); 69 | if (context === null) { 70 | return; 71 | } 72 | 73 | // Draw the image on the canvas (cropped and resized) 74 | context.drawImage( 75 | video, 76 | sourceX, 77 | sourceY, 78 | sourceWidth, 79 | sourceHeight, 80 | 0, 81 | 0, 82 | width, 83 | height, 84 | ); 85 | 86 | // Callback with frame data 87 | if (onFrameUpdate) { 88 | canvas.toBlob( 89 | (blob) => { 90 | blob?.arrayBuffer().then((buffer) => { 91 | const frameData = new Uint8Array(buffer); 92 | onFrameUpdate(frameData); 93 | }); 94 | }, 95 | "image/jpeg", 96 | 0.7, 97 | ); 98 | } 99 | }; 100 | 101 | useEffect(() => { 102 | const interval = setInterval(() => { 103 | captureFrame(); 104 | }, 16); // Adjust interval as needed 105 | 106 | return () => clearInterval(interval); 107 | }); 108 | }; 109 | 110 | type LCMInput = { 111 | prompt: string; 112 | image_bytes: Uint8Array; 113 | strength?: number; 114 | negative_prompt?: string; 115 | seed?: number | null; 116 | guidance_scale?: number; 117 | num_inference_steps?: number; 118 | enable_safety_checks?: boolean; 119 | request_id?: string; 120 | height?: number; 121 | width?: number; 122 | }; 123 | 124 | type ImageOutput = { 125 | content: Uint8Array; 126 | width: number; 127 | height: number; 128 | }; 129 | 130 | type LCMOutput = { 131 | images: ImageOutput[]; 132 | timings: Record; 133 | seed: number; 134 | num_inference_steps: number; 135 | request_id: string; 136 | nsfw_content_detected: boolean[]; 137 | }; 138 | 139 | export default function WebcamPage() { 140 | const [enabled, setEnabled] = useState(false); 141 | const processedImageRef = useRef(null); 142 | const videoRef = useRef(null); 143 | const previewRef = useRef(null); 144 | 145 | const { send } = fal.realtime.connect( 146 | "fal-ai/fast-turbo-diffusion/image-to-image", 147 | { 148 | connectionKey: "camera-turbo-demo", 149 | // not throttling the client, handling throttling of the camera itself 150 | // and letting all requests through in real-time 151 | throttleInterval: 0, 152 | onResult(result) { 153 | if (processedImageRef.current && result.images && result.images[0]) { 154 | const blob = new Blob([result.images[0].content], { 155 | type: "image/jpeg", 156 | }); 157 | const url = URL.createObjectURL(blob); 158 | processedImageRef.current.src = url; 159 | } 160 | }, 161 | }, 162 | ); 163 | 164 | const onFrameUpdate = (data: Uint8Array) => { 165 | if (!enabled) { 166 | return; 167 | } 168 | send({ 169 | prompt: "a picture of george clooney, elegant, in a suit, 8k, uhd", 170 | image_bytes: data, 171 | num_inference_steps: 3, 172 | strength: 0.6, 173 | guidance_scale: 1, 174 | seed: 6252023, 175 | }); 176 | }; 177 | 178 | useWebcam({ 179 | videoRef, 180 | previewRef, 181 | onFrameUpdate, 182 | }); 183 | 184 | return ( 185 |
186 |

187 | falcamera 188 |

189 | 190 |
191 | 199 |
200 |
201 | 202 | generated 210 |
211 |
212 | ); 213 | } 214 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/comfy/image-to-image/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import { createFalClient, Result } from "@fal-ai/client"; 5 | import { useMemo, useState } from "react"; 6 | 7 | const fal = createFalClient({ 8 | proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy 9 | // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy 10 | }); 11 | 12 | type Image = { 13 | filename: string; 14 | subfolder: string; 15 | type: string; 16 | url: string; 17 | }; 18 | 19 | type ComfyOutput = { 20 | url: string; 21 | outputs: Record[]; 22 | images: Image[]; 23 | }; 24 | 25 | type ErrorProps = { 26 | error: any; 27 | }; 28 | 29 | function Error(props: ErrorProps) { 30 | if (!props.error) { 31 | return null; 32 | } 33 | return ( 34 |
38 | Error {props.error.message} 39 |
40 | ); 41 | } 42 | 43 | const DEFAULT_PROMPT = 44 | "photograph of victorian woman with wings, sky clouds, meadow grass"; 45 | 46 | export default function ComfyImageToImagePage() { 47 | // @snippet:start("client.ui.state") 48 | // Input state 49 | const [prompt, setPrompt] = useState(DEFAULT_PROMPT); 50 | const [imageFile, setImageFile] = useState(null); 51 | // Result state 52 | const [loading, setLoading] = useState(false); 53 | const [error, setError] = useState(null); 54 | const [result, setResult] = useState(null); 55 | const [logs, setLogs] = useState([]); 56 | const [elapsedTime, setElapsedTime] = useState(0); 57 | // @snippet:end 58 | const video = useMemo(() => { 59 | if (!result) { 60 | return null; 61 | } 62 | return result; 63 | }, [result]); 64 | 65 | const reset = () => { 66 | setLoading(false); 67 | setError(null); 68 | setResult(null); 69 | setLogs([]); 70 | setElapsedTime(0); 71 | }; 72 | 73 | const getImageURL = (result: ComfyOutput) => { 74 | return result.outputs[9].images[0]; 75 | }; 76 | 77 | const generateVideo = async () => { 78 | reset(); 79 | // @snippet:start("client.queue.subscribe") 80 | setLoading(true); 81 | const start = Date.now(); 82 | try { 83 | const { data }: Result = await fal.subscribe( 84 | "comfy/fal-ai/image-to-image", 85 | { 86 | input: { 87 | prompt: prompt, 88 | loadimage_1: imageFile, 89 | }, 90 | logs: true, 91 | onQueueUpdate(update) { 92 | setElapsedTime(Date.now() - start); 93 | if ( 94 | update.status === "IN_PROGRESS" || 95 | update.status === "COMPLETED" 96 | ) { 97 | setLogs((update.logs || []).map((log) => log.message)); 98 | } 99 | }, 100 | }, 101 | ); 102 | setResult(getImageURL(data)); 103 | } catch (error: any) { 104 | setError(error); 105 | } finally { 106 | setLoading(false); 107 | setElapsedTime(Date.now() - start); 108 | } 109 | // @snippet:end 110 | }; 111 | return ( 112 |
113 |
114 |

115 | Comfy SD1.5 - Image to Image 116 |

117 |
118 | 121 |
122 |
123 | {imageFile && ( 124 | 129 | )} 130 |
131 | 132 | setImageFile(e.target.files?.[0] ?? null)} 140 | /> 141 |
142 |
143 |
144 | 147 | setPrompt(e.target.value)} 155 | onBlur={(e) => setPrompt(e.target.value.trim())} 156 | /> 157 |
158 | 159 | 169 | 170 | 171 | 172 |
173 |
174 | {video && ( 175 | // eslint-disable-next-line @next/next/no-img-element 176 | 177 | )} 178 |
179 |
180 |

JSON Result

181 |

182 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 183 |

184 |
185 |               {result
186 |                 ? JSON.stringify(result, null, 2)
187 |                 : "// result pending..."}
188 |             
189 |
190 | 191 |
192 |

Logs

193 |
194 |               {logs.filter(Boolean).join("\n")}
195 |             
196 |
197 |
198 |
199 |
200 | ); 201 | } 202 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/comfy/image-to-video/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createFalClient, Result } from "@fal-ai/client"; 4 | import { useMemo, useState } from "react"; 5 | 6 | const fal = createFalClient({ 7 | proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy 8 | // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy 9 | }); 10 | 11 | type Image = { 12 | filename: string; 13 | subfolder: string; 14 | type: string; 15 | url: string; 16 | }; 17 | 18 | type ComfyOutput = { 19 | url: string; 20 | outputs: Record[]; 21 | images: Image[]; 22 | }; 23 | 24 | type ErrorProps = { 25 | error: any; 26 | }; 27 | 28 | function Error(props: ErrorProps) { 29 | if (!props.error) { 30 | return null; 31 | } 32 | return ( 33 |
37 | Error {props.error.message} 38 |
39 | ); 40 | } 41 | 42 | export default function ComfyImageToVideoPage() { 43 | // @snippet:start("client.ui.state") 44 | // Input state 45 | const [imageFile, setImageFile] = useState(null); 46 | // Result state 47 | const [loading, setLoading] = useState(false); 48 | const [error, setError] = useState(null); 49 | const [result, setResult] = useState(null); 50 | const [logs, setLogs] = useState([]); 51 | const [elapsedTime, setElapsedTime] = useState(0); 52 | // @snippet:end 53 | const video = useMemo(() => { 54 | if (!result) { 55 | return null; 56 | } 57 | return result; 58 | }, [result]); 59 | 60 | const reset = () => { 61 | setLoading(false); 62 | setError(null); 63 | setResult(null); 64 | setLogs([]); 65 | setElapsedTime(0); 66 | }; 67 | 68 | const getImageURL = (result: ComfyOutput) => { 69 | return result.outputs[10].images[0]; 70 | }; 71 | 72 | const generateVideo = async () => { 73 | reset(); 74 | // @snippet:start("client.queue.subscribe") 75 | setLoading(true); 76 | const start = Date.now(); 77 | try { 78 | const { data }: Result = await fal.subscribe( 79 | "comfy/fal-ai/image-to-video", 80 | { 81 | input: { 82 | loadimage_1: imageFile, 83 | }, 84 | logs: true, 85 | onQueueUpdate(update) { 86 | setElapsedTime(Date.now() - start); 87 | if ( 88 | update.status === "IN_PROGRESS" || 89 | update.status === "COMPLETED" 90 | ) { 91 | setLogs((update.logs || []).map((log) => log.message)); 92 | } 93 | }, 94 | }, 95 | ); 96 | setResult(getImageURL(data)); 97 | } catch (error: any) { 98 | setError(error); 99 | } finally { 100 | setLoading(false); 101 | setElapsedTime(Date.now() - start); 102 | } 103 | // @snippet:end 104 | }; 105 | return ( 106 |
107 |
108 |

Comfy SVD - Image to Video

109 |
110 | 113 |
114 |
115 | {imageFile && ( 116 | 121 | )} 122 |
123 | 124 | setImageFile(e.target.files?.[0] ?? null)} 132 | /> 133 |
134 |
135 | 136 | 146 | 147 | 148 | 149 |
150 |
151 | {video && ( 152 | // eslint-disable-next-line @next/next/no-img-element 153 | 154 | )} 155 |
156 |
157 |

JSON Result

158 |

159 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 160 |

161 |
162 |               {result
163 |                 ? JSON.stringify(result, null, 2)
164 |                 : "// result pending..."}
165 |             
166 |
167 | 168 |
169 |

Logs

170 |
171 |               {logs.filter(Boolean).join("\n")}
172 |             
173 |
174 |
175 |
176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/comfy/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | export default function Index() { 6 | const router = useRouter(); // Use correct router 7 | return ( 8 |
9 |
10 |

11 | Serverless Comfy Workflow Examples powered by{" "} 12 | fal 13 |

14 |

15 | Learn how to use our fal-js to execute Comfy workflows. 16 |

17 |
18 | 24 | 30 | 36 |
37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/comfy/text-to-image/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createFalClient, Result } from "@fal-ai/client"; 4 | import { useMemo, useState } from "react"; 5 | 6 | const fal = createFalClient({ 7 | proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy 8 | // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy 9 | }); 10 | 11 | type Image = { 12 | filename: string; 13 | subfolder: string; 14 | type: string; 15 | url: string; 16 | }; 17 | 18 | type ComfyOutput = { 19 | url: string; 20 | outputs: Record[]; 21 | images: Image[]; 22 | }; 23 | 24 | type ErrorProps = { 25 | error: any; 26 | }; 27 | 28 | function Error(props: ErrorProps) { 29 | if (!props.error) { 30 | return null; 31 | } 32 | return ( 33 |
37 | Error {props.error.message} 38 |
39 | ); 40 | } 41 | 42 | const DEFAULT_PROMPT = 43 | "a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd"; 44 | 45 | export default function ComfyTextToImagePage() { 46 | // @snippet:start("client.ui.state") 47 | // Input state 48 | const [prompt, setPrompt] = useState(DEFAULT_PROMPT); 49 | // Result state 50 | const [loading, setLoading] = useState(false); 51 | const [error, setError] = useState(null); 52 | const [result, setResult] = useState(null); 53 | const [logs, setLogs] = useState([]); 54 | const [elapsedTime, setElapsedTime] = useState(0); 55 | // @snippet:end 56 | const image = useMemo(() => { 57 | if (!result) { 58 | return null; 59 | } 60 | return result; 61 | }, [result]); 62 | 63 | const reset = () => { 64 | setLoading(false); 65 | setError(null); 66 | setResult(null); 67 | setLogs([]); 68 | setElapsedTime(0); 69 | }; 70 | 71 | const getImageURL = (result: ComfyOutput) => { 72 | return result.outputs[9].images[0]; 73 | }; 74 | 75 | const generateImage = async () => { 76 | reset(); 77 | // @snippet:start("client.queue.subscribe") 78 | setLoading(true); 79 | const start = Date.now(); 80 | try { 81 | const { data }: Result = await fal.subscribe( 82 | "comfy/fal-ai/text-to-image", 83 | { 84 | input: { 85 | prompt: prompt, 86 | }, 87 | logs: true, 88 | onQueueUpdate(update) { 89 | setElapsedTime(Date.now() - start); 90 | if ( 91 | update.status === "IN_PROGRESS" || 92 | update.status === "COMPLETED" 93 | ) { 94 | setLogs((update.logs || []).map((log) => log.message)); 95 | } 96 | }, 97 | }, 98 | ); 99 | setResult(getImageURL(data)); 100 | } catch (error: any) { 101 | setError(error); 102 | } finally { 103 | setLoading(false); 104 | setElapsedTime(Date.now() - start); 105 | } 106 | // @snippet:end 107 | }; 108 | return ( 109 |
110 |
111 |

Comfy SDXL - Text to Image

112 |
113 | 116 | setPrompt(e.target.value)} 124 | onBlur={(e) => setPrompt(e.target.value.trim())} 125 | /> 126 |
127 | 128 | 138 | 139 | 140 | 141 |
142 |
143 | {image && ( 144 | // eslint-disable-next-line @next/next/no-img-element 145 | 146 | )} 147 |
148 |
149 |

JSON Result

150 |

151 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 152 |

153 |
154 |               {result
155 |                 ? JSON.stringify(result, null, 2)
156 |                 : "// result pending..."}
157 |             
158 |
159 | 160 |
161 |

Logs

162 |
163 |               {logs.filter(Boolean).join("\n")}
164 |             
165 |
166 |
167 |
168 |
169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | 3 | export const metadata = { 4 | title: "Welcome to demo-nextjs-app-router", 5 | description: "Generated by create-nx-workspace", 6 | }; 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createFalClient } from "@fal-ai/client"; 4 | import { IllusionDiffusionOutput } from "@fal-ai/client/endpoints"; 5 | import { useMemo, useState } from "react"; 6 | 7 | const fal = createFalClient({ 8 | // credentials: 'FAL_KEY_ID:FAL_KEY_SECRET', 9 | proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy 10 | // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy 11 | }); 12 | 13 | type ErrorProps = { 14 | error: any; 15 | }; 16 | 17 | function Error(props: ErrorProps) { 18 | if (!props.error) { 19 | return null; 20 | } 21 | return ( 22 |
26 | Error {props.error.message} 27 |
28 | ); 29 | } 30 | 31 | const DEFAULT_PROMPT = 32 | "(masterpiece:1.4), (best quality), (detailed), Medieval village scene with busy streets and castle in the distance"; 33 | 34 | export default function Home() { 35 | // @snippet:start("client.ui.state") 36 | // Input state 37 | const [prompt, setPrompt] = useState(DEFAULT_PROMPT); 38 | const [imageFile, setImageFile] = useState(null); 39 | // Result state 40 | const [loading, setLoading] = useState(false); 41 | const [error, setError] = useState(null); 42 | const [result, setResult] = useState(null); 43 | const [logs, setLogs] = useState([]); 44 | const [elapsedTime, setElapsedTime] = useState(0); 45 | // @snippet:end 46 | const image = useMemo(() => { 47 | if (!result) { 48 | return null; 49 | } 50 | if (result.image) { 51 | return result.image; 52 | } 53 | return null; 54 | }, [result]); 55 | 56 | const reset = () => { 57 | setLoading(false); 58 | setError(null); 59 | setResult(null); 60 | setLogs([]); 61 | setElapsedTime(0); 62 | }; 63 | 64 | const generateImage = async () => { 65 | if (!imageFile) return; 66 | reset(); 67 | // @snippet:start("client.queue.subscribe") 68 | setLoading(true); 69 | const start = Date.now(); 70 | try { 71 | const result = await fal.subscribe("fal-ai/illusion-diffusion", { 72 | input: { 73 | prompt, 74 | image_url: imageFile, 75 | image_size: "square_hd", 76 | }, 77 | logs: true, 78 | onQueueUpdate(update) { 79 | setElapsedTime(Date.now() - start); 80 | if ( 81 | update.status === "IN_PROGRESS" || 82 | update.status === "COMPLETED" 83 | ) { 84 | setLogs((update.logs || []).map((log) => log.message)); 85 | } 86 | }, 87 | }); 88 | setResult(result.data); 89 | } catch (error: any) { 90 | setError(error); 91 | } finally { 92 | setLoading(false); 93 | setElapsedTime(Date.now() - start); 94 | } 95 | // @snippet:end 96 | }; 97 | return ( 98 |
99 |
100 |

101 | Hello fal 102 |

103 |
104 | 107 | setImageFile(e.target.files?.[0] ?? null)} 115 | /> 116 |
117 |
118 | 121 | setPrompt(e.target.value)} 129 | onBlur={(e) => setPrompt(e.target.value.trim())} 130 | /> 131 |
132 | 133 | 143 | 144 | 145 | 146 |
147 |
148 | {image && ( 149 | // eslint-disable-next-line @next/next/no-img-element 150 | 151 | )} 152 |
153 |
154 |

JSON Result

155 |

156 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 157 |

158 |
159 |               {result
160 |                 ? JSON.stringify(result, null, 2)
161 |                 : "// result pending..."}
162 |             
163 |
164 | 165 |
166 |

Logs

167 |
168 |               {logs.filter(Boolean).join("\n")}
169 |             
170 |
171 |
172 |
173 |
174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/queue/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { fal } from "@fal-ai/client"; 4 | import { useState } from "react"; 5 | 6 | fal.config({ 7 | proxyUrl: "/api/fal/proxy", 8 | }); 9 | 10 | type ErrorProps = { 11 | error: any; 12 | }; 13 | 14 | function Error(props: ErrorProps) { 15 | if (!props.error) { 16 | return null; 17 | } 18 | return ( 19 |
23 | Error {props.error.message} 24 |
25 | ); 26 | } 27 | 28 | const DEFAULT_ENDPOINT_ID = "fal-ai/fast-sdxl"; 29 | const DEFAULT_INPUT = `{ 30 | "prompt": "A beautiful sunset over the ocean" 31 | }`; 32 | 33 | export default function Home() { 34 | // Input state 35 | const [endpointId, setEndpointId] = useState(DEFAULT_ENDPOINT_ID); 36 | const [input, setInput] = useState(DEFAULT_INPUT); 37 | // Result state 38 | const [loading, setLoading] = useState(false); 39 | const [error, setError] = useState(null); 40 | const [result, setResult] = useState(null); 41 | const [logs, setLogs] = useState([]); 42 | const [elapsedTime, setElapsedTime] = useState(0); 43 | 44 | const reset = () => { 45 | setLoading(false); 46 | setError(null); 47 | setResult(null); 48 | setLogs([]); 49 | setElapsedTime(0); 50 | }; 51 | 52 | const run = async () => { 53 | reset(); 54 | setLoading(true); 55 | const start = Date.now(); 56 | try { 57 | const result = await fal.subscribe(endpointId, { 58 | input: JSON.parse(input), 59 | logs: true, 60 | // mode: "streaming", 61 | mode: "polling", 62 | pollInterval: 1000, 63 | onQueueUpdate(update) { 64 | console.log("queue update"); 65 | console.log(update); 66 | setElapsedTime(Date.now() - start); 67 | if ( 68 | update.status === "IN_PROGRESS" || 69 | update.status === "COMPLETED" 70 | ) { 71 | if (update.logs && update.logs.length > logs.length) { 72 | setLogs((update.logs || []).map((log) => log.message)); 73 | } 74 | } 75 | }, 76 | }); 77 | setResult(result); 78 | } catch (error: any) { 79 | setError(error); 80 | } finally { 81 | setLoading(false); 82 | setElapsedTime(Date.now() - start); 83 | } 84 | }; 85 | return ( 86 |
87 |
88 |

89 | fal 90 | queue 91 |

92 |
93 | 96 | setEndpointId(e.target.value)} 105 | /> 106 |
107 |
108 | 111 | 122 |
123 | 124 | 134 | 135 | 136 | 137 |
138 |
139 |

JSON Result

140 |

141 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 142 |

143 |
144 |               {result
145 |                 ? JSON.stringify(result, null, 2)
146 |                 : "// result pending..."}
147 |             
148 |
149 | 150 |
151 |

Logs

152 |
153 |               {logs.join("\n")}
154 |             
155 |
156 |
157 |
158 |
159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/realtime/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /* eslint-disable @next/next/no-img-element */ 4 | import { createFalClient } from "@fal-ai/client"; 5 | import { ChangeEvent, useRef, useState } from "react"; 6 | import { DrawingCanvas } from "../../components/drawing"; 7 | 8 | const fal = createFalClient({ 9 | proxyUrl: "/api/fal/proxy", 10 | }); 11 | 12 | const PROMPT_EXPANDED = 13 | ", beautiful, colorful, highly detailed, best quality, uhd"; 14 | 15 | const PROMPT = "a moon in the night sky"; 16 | 17 | const defaults = { 18 | model_name: "runwayml/stable-diffusion-v1-5", 19 | image_size: "square", 20 | num_inference_steps: 4, 21 | seed: 6252023, 22 | }; 23 | 24 | export default function RealtimePage() { 25 | const [prompt, setPrompt] = useState(PROMPT); 26 | 27 | const currentDrawing = useRef(null); 28 | const outputCanvasRef = useRef(null); 29 | 30 | const { send } = fal.realtime.connect( 31 | "fal-ai/fast-lcm-diffusion/image-to-image", 32 | { 33 | connectionKey: "realtime-demo", 34 | throttleInterval: 128, 35 | onResult(result) { 36 | if (result.images && result.images[0] && result.images[0].content) { 37 | const canvas = outputCanvasRef.current; 38 | const context = canvas?.getContext("2d"); 39 | if (canvas && context) { 40 | const imageBytes: Uint8Array = result.images[0].content; 41 | const blob = new Blob([imageBytes], { type: "image/png" }); 42 | createImageBitmap(blob) 43 | .then((bitmap) => { 44 | context.drawImage(bitmap, 0, 0); 45 | }) 46 | .catch(console.error); 47 | } 48 | } 49 | }, 50 | }, 51 | ); 52 | 53 | const handlePromptChange = (e: ChangeEvent) => { 54 | setPrompt(e.target.value); 55 | if (currentDrawing.current) { 56 | send({ 57 | prompt: e.target.value.trim() + PROMPT_EXPANDED, 58 | image_bytes: currentDrawing.current, 59 | ...defaults, 60 | }); 61 | } 62 | }; 63 | 64 | return ( 65 |
66 |
67 |

68 | falrealtime 69 |

70 |
71 | 76 |
77 |
78 |
79 | { 81 | currentDrawing.current = imageData; 82 | send({ 83 | prompt: prompt + PROMPT_EXPANDED, 84 | image_bytes: imageData, 85 | ...defaults, 86 | }); 87 | }} 88 | /> 89 |
90 |
91 |
92 | 98 |
99 |
100 |
101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/streaming/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { fal } from "@fal-ai/client"; 4 | import { useState } from "react"; 5 | 6 | fal.config({ 7 | proxyUrl: "/api/fal/proxy", 8 | }); 9 | 10 | type ErrorProps = { 11 | error: any; 12 | }; 13 | 14 | function Error(props: ErrorProps) { 15 | if (!props.error) { 16 | return null; 17 | } 18 | return ( 19 |
23 | Error {props.error.message} 24 |
25 | ); 26 | } 27 | 28 | const DEFAULT_ENDPOINT_ID = "fal-ai/llavav15-13b"; 29 | const DEFAULT_INPUT = { 30 | prompt: "Do you know who drew this picture and what is the name of it?", 31 | image_url: "https://llava-vl.github.io/static/images/monalisa.jpg", 32 | max_new_tokens: 100, 33 | temperature: 0.2, 34 | top_p: 1, 35 | }; 36 | 37 | export default function StreamingTest() { 38 | // Input state 39 | const [endpointId, setEndpointId] = useState(DEFAULT_ENDPOINT_ID); 40 | const [input, setInput] = useState( 41 | JSON.stringify(DEFAULT_INPUT, null, 2), 42 | ); 43 | // Result state 44 | const [loading, setLoading] = useState(false); 45 | const [error, setError] = useState(null); 46 | const [events, setEvents] = useState([]); 47 | const [elapsedTime, setElapsedTime] = useState(0); 48 | 49 | const reset = () => { 50 | setLoading(false); 51 | setError(null); 52 | setEvents([]); 53 | setElapsedTime(0); 54 | }; 55 | 56 | const run = async () => { 57 | reset(); 58 | setLoading(true); 59 | const start = Date.now(); 60 | try { 61 | const stream = await fal.stream(endpointId, { 62 | input: JSON.parse(input), 63 | }); 64 | 65 | for await (const partial of stream) { 66 | setEvents((events) => [partial, ...events]); 67 | } 68 | 69 | const result = await stream.done(); 70 | setEvents((events) => [result, ...events]); 71 | } catch (error: any) { 72 | setError(error); 73 | } finally { 74 | setLoading(false); 75 | setElapsedTime(Date.now() - start); 76 | } 77 | }; 78 | return ( 79 |
80 |
81 |

82 | fal 83 | queue 84 |

85 |
86 | 89 | setEndpointId(e.target.value)} 98 | /> 99 |
100 |
101 | 104 | 115 |
116 | 117 | 127 | 128 | 129 | 130 |
131 |
132 |

JSON Result

133 |

134 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 135 |

136 |
137 | {events.map((event, index) => ( 138 |
142 |                   {JSON.stringify(event, null, 2)}
143 |                 
144 | ))} 145 |
146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/app/whisper/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createFalClient } from "@fal-ai/client"; 4 | import { useCallback, useMemo, useState } from "react"; 5 | 6 | const fal = createFalClient({ 7 | // credentials: 'FAL_KEY_ID:FAL_KEY_SECRET', 8 | proxyUrl: "/api/fal/proxy", 9 | }); 10 | 11 | type ErrorProps = { 12 | error: any; 13 | }; 14 | 15 | function Error(props: ErrorProps) { 16 | if (!props.error) { 17 | return null; 18 | } 19 | return ( 20 |
24 | Error {props.error.message} 25 |
26 | ); 27 | } 28 | 29 | type RecorderOptions = { 30 | maxDuration?: number; 31 | }; 32 | 33 | function useMediaRecorder({ maxDuration = 10000 }: RecorderOptions = {}) { 34 | const [isRecording, setIsRecording] = useState(false); 35 | const [mediaRecorder, setMediaRecorder] = useState( 36 | null, 37 | ); 38 | 39 | const record = useCallback(async () => { 40 | setIsRecording(true); 41 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 42 | const audioChunks: BlobPart[] = []; 43 | const recorder = new MediaRecorder(stream); 44 | setMediaRecorder(recorder); 45 | return new Promise((resolve, reject) => { 46 | try { 47 | recorder.addEventListener("dataavailable", (event) => { 48 | audioChunks.push(event.data); 49 | }); 50 | recorder.addEventListener("stop", async () => { 51 | const fileOptions = { type: "audio/wav" }; 52 | const audioBlob = new Blob(audioChunks, fileOptions); 53 | const audioFile = new File( 54 | [audioBlob], 55 | `recording_${Date.now()}.wav`, 56 | fileOptions, 57 | ); 58 | setIsRecording(false); 59 | resolve(audioFile); 60 | }); 61 | setTimeout(() => { 62 | recorder.stop(); 63 | recorder.stream.getTracks().forEach((track) => track.stop()); 64 | }, maxDuration); 65 | recorder.start(); 66 | } catch (error) { 67 | reject(error); 68 | } 69 | }); 70 | }, [maxDuration]); 71 | 72 | const stopRecording = useCallback(() => { 73 | setIsRecording(false); 74 | mediaRecorder?.stop(); 75 | mediaRecorder?.stream.getTracks().forEach((track) => track.stop()); 76 | }, [mediaRecorder]); 77 | 78 | return { record, stopRecording, isRecording }; 79 | } 80 | 81 | export default function WhisperDemo() { 82 | const [loading, setLoading] = useState(false); 83 | const [error, setError] = useState(null); 84 | const [logs, setLogs] = useState([]); 85 | const [audioFile, setAudioFile] = useState(null); 86 | const [result, setResult] = useState(null); // eslint-disable-line @typescript-eslint/no-explicit-any 87 | const [elapsedTime, setElapsedTime] = useState(0); 88 | 89 | const { record, stopRecording, isRecording } = useMediaRecorder(); 90 | 91 | const reset = () => { 92 | setLoading(false); 93 | setError(null); 94 | setLogs([]); 95 | setElapsedTime(0); 96 | setResult(null); 97 | }; 98 | 99 | const audioFileLocalUrl = useMemo(() => { 100 | if (!audioFile) { 101 | return null; 102 | } 103 | return URL.createObjectURL(audioFile); 104 | }, [audioFile]); 105 | 106 | const transcribeAudio = async (audioFile: File) => { 107 | reset(); 108 | setLoading(true); 109 | const start = Date.now(); 110 | try { 111 | const result = await fal.subscribe("fal-ai/wizper", { 112 | input: { 113 | audio_url: audioFile, 114 | version: "3", 115 | }, 116 | logs: true, 117 | onQueueUpdate(update) { 118 | setElapsedTime(Date.now() - start); 119 | if ( 120 | update.status === "IN_PROGRESS" || 121 | update.status === "COMPLETED" 122 | ) { 123 | setLogs((update.logs || []).map((log) => log.message)); 124 | } 125 | }, 126 | }); 127 | setResult(result); 128 | } catch (error: any) { 129 | setError(error); 130 | } finally { 131 | setLoading(false); 132 | setElapsedTime(Date.now() - start); 133 | } 134 | }; 135 | return ( 136 |
137 |
138 |

139 | Hello fal and{" "} 140 | whisper 141 |

142 | 143 |
144 | 163 | 179 |
180 | 181 | {audioFileLocalUrl && ( 182 |
183 |
185 | )} 186 | 187 | 188 | 189 |
190 |
191 |

JSON Result

192 |

193 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 194 |

195 |
196 |               {result
197 |                 ? JSON.stringify(result, null, 2)
198 |                 : "// result pending..."}
199 |             
200 |
201 | 202 |
203 |

Logs

204 |
205 |               {logs.filter(Boolean).join("\n")}
206 |             
207 |
208 |
209 |
210 |
211 | ); 212 | } 213 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/components/drawing.tsx: -------------------------------------------------------------------------------- 1 | import { type Excalidraw } from "@excalidraw/excalidraw"; 2 | import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types"; 3 | import { 4 | AppState, 5 | ExcalidrawImperativeAPI, 6 | } from "@excalidraw/excalidraw/types/types"; 7 | import { useCallback, useEffect, useState } from "react"; 8 | import initialDrawing from "./drawingState.json"; 9 | 10 | export type CanvasChangeEvent = { 11 | elements: readonly ExcalidrawElement[]; 12 | appState: AppState; 13 | imageData: Uint8Array; 14 | }; 15 | 16 | export type DrawingCanvasProps = { 17 | onCanvasChange: (event: CanvasChangeEvent) => void; 18 | }; 19 | 20 | export async function blobToBase64(blob: Blob): Promise { 21 | const reader = new FileReader(); 22 | reader.readAsDataURL(blob); 23 | return new Promise((resolve) => { 24 | reader.onloadend = () => { 25 | resolve(reader.result?.toString() || ""); 26 | }; 27 | }); 28 | } 29 | 30 | export async function blobToUint8Array(blob: Blob): Promise { 31 | const buffer = await blob.arrayBuffer(); 32 | return new Uint8Array(buffer); 33 | } 34 | 35 | export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) { 36 | const [ExcalidrawComponent, setExcalidrawComponent] = useState< 37 | typeof Excalidraw | null 38 | >(null); 39 | const [excalidrawAPI, setExcalidrawAPI] = 40 | useState(null); 41 | const [sceneData, setSceneData] = useState(null); 42 | 43 | useEffect(() => { 44 | import("@excalidraw/excalidraw").then((comp) => 45 | setExcalidrawComponent(comp.Excalidraw), 46 | ); 47 | const onResize = () => { 48 | if (excalidrawAPI) { 49 | excalidrawAPI.refresh(); 50 | } 51 | }; 52 | window.addEventListener("resize", onResize); 53 | return () => { 54 | window.removeEventListener("resize", onResize); 55 | }; 56 | }, []); 57 | 58 | const handleCanvasChanges = useCallback( 59 | async (elements: readonly ExcalidrawElement[], appState: AppState) => { 60 | if (!excalidrawAPI || !elements || !elements.length) { 61 | return; 62 | } 63 | const { exportToBlob, convertToExcalidrawElements, serializeAsJSON } = 64 | await import("@excalidraw/excalidraw"); 65 | 66 | const [boundingBoxElement] = convertToExcalidrawElements([ 67 | { 68 | type: "rectangle", 69 | x: 0, 70 | y: 0, 71 | width: 512, 72 | height: 512, 73 | fillStyle: "solid", 74 | backgroundColor: "cyan", 75 | }, 76 | ]); 77 | 78 | const newSceneData = serializeAsJSON( 79 | elements, 80 | appState, 81 | excalidrawAPI.getFiles(), 82 | "local", 83 | ); 84 | if (newSceneData !== sceneData) { 85 | setSceneData(newSceneData); 86 | const blob = await exportToBlob({ 87 | elements: [boundingBoxElement, ...elements], 88 | appState: { 89 | ...appState, 90 | frameRendering: { 91 | ...(appState.frameRendering || {}), 92 | clip: false, 93 | }, 94 | }, 95 | files: excalidrawAPI.getFiles(), 96 | mimeType: "image/webp", 97 | quality: 0.5, 98 | exportPadding: 0, 99 | getDimensions: () => { 100 | return { width: 512, height: 512 }; 101 | }, 102 | }); 103 | const imageData = await blobToUint8Array(blob); 104 | onCanvasChange({ elements, appState, imageData }); 105 | } 106 | }, 107 | [excalidrawAPI, onCanvasChange, sceneData], 108 | ); 109 | 110 | return ( 111 |
112 | {ExcalidrawComponent && ( 113 | setExcalidrawAPI(api)} 115 | initialData={{ elements: initialDrawing as ExcalidrawElement[] }} 116 | onChange={handleCanvasChanges} 117 | /> 118 | )} 119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/components/drawingState.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "rectangle", 4 | "version": 240, 5 | "versionNonce": 21728473, 6 | "isDeleted": false, 7 | "id": "EnLu91BTRnzWtj7m-l4Id", 8 | "fillStyle": "solid", 9 | "strokeWidth": 2, 10 | "strokeStyle": "solid", 11 | "roughness": 1, 12 | "opacity": 100, 13 | "angle": 0, 14 | "x": -3.3853912353515625, 15 | "y": -2.3741912841796875, 16 | "strokeColor": "#1971c2", 17 | "backgroundColor": "#343a40", 18 | "width": 568.016487121582, 19 | "height": 582.1398010253906, 20 | "seed": 295965933, 21 | "groupIds": [], 22 | "frameId": null, 23 | "roundness": null, 24 | "boundElements": [], 25 | "updated": 1700904828477, 26 | "link": null, 27 | "locked": false 28 | }, 29 | { 30 | "type": "ellipse", 31 | "version": 3545, 32 | "versionNonce": 647409943, 33 | "isDeleted": false, 34 | "id": "F6oN3k42RqfCqlzJLGXXS", 35 | "fillStyle": "solid", 36 | "strokeWidth": 1, 37 | "strokeStyle": "solid", 38 | "roughness": 1, 39 | "opacity": 100, 40 | "angle": 0, 41 | "x": 345.65307998657227, 42 | "y": 81.02682495117188, 43 | "strokeColor": "#f08c00", 44 | "backgroundColor": "#ffec99", 45 | "width": 124.31249999999997, 46 | "height": 113.591796875, 47 | "seed": 23374002, 48 | "groupIds": [], 49 | "frameId": null, 50 | "roundness": { 51 | "type": 2 52 | }, 53 | "boundElements": [], 54 | "updated": 1700904844024, 55 | "link": null, 56 | "locked": false 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module "*.svg" { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "demo-nextjs-app-router", 4 | preset: "../../jest.preset.js", 5 | transform: { 6 | "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest", 7 | "^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/next/babel"] }], 8 | }, 9 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 10 | coverageDirectory: "../../coverage/apps/demo-nextjs-app-router", 11 | }; 12 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/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 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/next.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { composePlugins, withNx } = require("@nx/next"); 5 | 6 | /** 7 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 8 | **/ 9 | const nextConfig = { 10 | nx: { 11 | // Set this to true if you would like to use SVGR 12 | // See: https://github.com/gregberge/svgr 13 | svgr: false, 14 | }, 15 | }; 16 | 17 | const plugins = [ 18 | // Add more Next.js plugins to this list if needed. 19 | withNx, 20 | ]; 21 | 22 | module.exports = composePlugins(...plugins)(nextConfig); 23 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/postcss.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | // Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build 4 | // option from your application's configuration (i.e. project.json). 5 | // 6 | // See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries 7 | 8 | module.exports = { 9 | plugins: { 10 | tailwindcss: { 11 | config: join(__dirname, "tailwind.config.js"), 12 | }, 13 | autoprefixer: {}, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-nextjs-app-router", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/demo-nextjs-app-router", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/next:build", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "outputPath": "dist/apps/demo-nextjs-app-router" 13 | }, 14 | "configurations": { 15 | "development": { 16 | "outputPath": "apps/demo-nextjs-app-router" 17 | }, 18 | "production": {} 19 | } 20 | }, 21 | "serve": { 22 | "executor": "@nx/next:server", 23 | "defaultConfiguration": "development", 24 | "options": { 25 | "buildTarget": "demo-nextjs-app-router:build", 26 | "dev": true 27 | }, 28 | "configurations": { 29 | "development": { 30 | "buildTarget": "demo-nextjs-app-router:build:development", 31 | "dev": true 32 | }, 33 | "production": { 34 | "buildTarget": "demo-nextjs-app-router:build:production", 35 | "dev": false 36 | } 37 | } 38 | }, 39 | "export": { 40 | "executor": "@nx/next:export", 41 | "options": { 42 | "buildTarget": "demo-nextjs-app-router:build:production" 43 | } 44 | }, 45 | "test": { 46 | "executor": "@nx/jest:jest", 47 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 48 | "options": { 49 | "jestConfig": "apps/demo-nextjs-app-router/jest.config.ts", 50 | "passWithNoTests": true 51 | }, 52 | "configurations": { 53 | "ci": { 54 | "ci": true, 55 | "codeCoverage": true 56 | } 57 | } 58 | }, 59 | "lint": { 60 | "executor": "@nx/linter:eslint", 61 | "outputs": ["{options.outputFile}"], 62 | "options": { 63 | "lintFilePatterns": ["apps/demo-nextjs-app-router/**/*.{ts,tsx,js,jsx}"] 64 | } 65 | } 66 | }, 67 | "tags": [] 68 | } 69 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/demo-nextjs-app-router/public/.gitkeep -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/demo-nextjs-app-router/public/favicon.ico -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require("@nx/react/tailwind"); 2 | const { join } = require("path"); 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | content: [ 7 | join( 8 | __dirname, 9 | "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}", 10 | ), 11 | ...createGlobPatternsForDependencies(__dirname), 12 | ], 13 | darkMode: "class", 14 | theme: { 15 | extend: {}, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "plugins": [ 15 | { 16 | "name": "next" 17 | } 18 | ], 19 | "types": ["jest", "node"] 20 | }, 21 | "include": [ 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js", 25 | "**/*.jsx", 26 | "../../apps/demo-nextjs-app-router/.next/types/**/*.ts", 27 | "../../dist/apps/demo-nextjs-app-router/.next/types/**/*.ts", 28 | "next-env.d.ts" 29 | ], 30 | "exclude": [ 31 | "node_modules", 32 | "jest.config.ts", 33 | "src/**/*.spec.ts", 34 | "src/**/*.test.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo-nextjs-app-router/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/.env.example: -------------------------------------------------------------------------------- 1 | # Rename this file to .env.local and add your fal credentials 2 | # Visit https://fal.ai to get started 3 | FAL_KEY="FAL_KEY_ID:FAL_KEY_SECRET" 4 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": ["!**/*", ".next/**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": ["error", "apps/demo-app/pages"] 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["*.js", "*.jsx"], 22 | "rules": {} 23 | } 24 | ], 25 | "rules": { 26 | "@next/next/no-html-link-for-pages": "off" 27 | }, 28 | "env": { 29 | "jest": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module "*.svg" { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "demo-nextjs-page-router", 4 | preset: "../../jest.preset.js", 5 | transform: { 6 | "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest", 7 | "^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/next/babel"] }], 8 | }, 9 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 10 | coverageDirectory: "../../coverage/apps/demo-nextjs-page-router", 11 | }; 12 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/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 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/next.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { withNx } = require("@nx/next/plugins/with-nx"); 5 | 6 | /** 7 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 8 | **/ 9 | const nextConfig = { 10 | nx: { 11 | // Set this to true if you would like to to use SVGR 12 | // See: https://github.com/gregberge/svgr 13 | svgr: false, 14 | }, 15 | }; 16 | 17 | module.exports = withNx(nextConfig); 18 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from "next/app"; 2 | import Head from "next/head"; 3 | import "./styles.css"; 4 | 5 | function CustomApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | Welcome to demo-app! 10 | 11 |
12 | 13 |
14 | 15 | ); 16 | } 17 | 18 | export default CustomApp; 19 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/pages/api/fal/proxy.ts: -------------------------------------------------------------------------------- 1 | // @snippet:start("client.proxy.nextjs") 2 | export { handler as default } from "@fal-ai/server-proxy/nextjs"; 3 | // @snippet:end 4 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/pages/index.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | } 3 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFalClient } from "@fal-ai/client"; 2 | import { useMemo, useState } from "react"; 3 | 4 | // @snippet:start(client.config) 5 | const fal = createFalClient({ 6 | proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy 7 | // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy 8 | }); 9 | // @snippet:end 10 | 11 | // @snippet:start(client.result.type) 12 | type Image = { 13 | url: string; 14 | file_name: string; 15 | file_size: number; 16 | }; 17 | type Output = { 18 | images: Image[]; 19 | }; 20 | // @snippet:end 21 | 22 | type ErrorProps = { 23 | error: any; 24 | }; 25 | 26 | function Error(props: ErrorProps) { 27 | if (!props.error) { 28 | return null; 29 | } 30 | return ( 31 |
35 | Error {props.error.message} 36 |
37 | ); 38 | } 39 | 40 | const DEFAULT_PROMPT = 41 | "a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd"; 42 | 43 | export function Index() { 44 | // @snippet:start("client.ui.state") 45 | // Input state 46 | const [prompt, setPrompt] = useState(DEFAULT_PROMPT); 47 | // Result state 48 | const [loading, setLoading] = useState(false); 49 | const [error, setError] = useState(null); 50 | const [result, setResult] = useState(null); 51 | const [logs, setLogs] = useState([]); 52 | const [elapsedTime, setElapsedTime] = useState(0); 53 | // @snippet:end 54 | const image = useMemo(() => { 55 | if (!result) { 56 | return null; 57 | } 58 | return result.images[0]; 59 | }, [result]); 60 | 61 | const reset = () => { 62 | setLoading(false); 63 | setError(null); 64 | setResult(null); 65 | setLogs([]); 66 | setElapsedTime(0); 67 | }; 68 | 69 | const generateImage = async () => { 70 | reset(); 71 | // @snippet:start("client.queue.subscribe") 72 | setLoading(true); 73 | const start = Date.now(); 74 | try { 75 | const result = await fal.subscribe("fal-ai/lora", { 76 | input: { 77 | prompt, 78 | model_name: "stabilityai/stable-diffusion-xl-base-1.0", 79 | image_size: "square_hd", 80 | }, 81 | logs: true, 82 | onQueueUpdate(update) { 83 | setElapsedTime(Date.now() - start); 84 | if ( 85 | update.status === "IN_PROGRESS" || 86 | update.status === "COMPLETED" 87 | ) { 88 | setLogs((update.logs || []).map((log) => log.message)); 89 | } 90 | }, 91 | }); 92 | setResult(result.data); 93 | } catch (error: any) { 94 | setError(error); 95 | } finally { 96 | setLoading(false); 97 | setElapsedTime(Date.now() - start); 98 | } 99 | // @snippet:end 100 | }; 101 | return ( 102 |
103 |
104 |

105 | Hello fal 106 |

107 |
108 | 111 | setPrompt(e.target.value)} 119 | onBlur={(e) => setPrompt(e.target.value.trim())} 120 | /> 121 |
122 | 123 | 133 | 134 | 135 | 136 |
137 |
138 | {image && ( 139 | // eslint-disable-next-line @next/next/no-img-element 140 | 141 | )} 142 |
143 |
144 |

JSON Result

145 |

146 | {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} 147 |

148 |
149 |               {result
150 |                 ? JSON.stringify(result, null, 2)
151 |                 : "// result pending..."}
152 |             
153 |
154 | 155 |
156 |

Logs

157 |
158 |               {logs.filter(Boolean).join("\n")}
159 |             
160 |
161 |
162 |
163 |
164 | ); 165 | } 166 | 167 | export default Index; 168 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/postcss.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | // Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build 4 | // option from your application's configuration (i.e. project.json). 5 | // 6 | // See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries 7 | 8 | module.exports = { 9 | plugins: { 10 | tailwindcss: { 11 | config: join(__dirname, "tailwind.config.js"), 12 | }, 13 | autoprefixer: {}, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-nextjs-page-router", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/demo-nextjs-page-router", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/next:build", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "outputPath": "dist/apps/demo-nextjs-page-router" 13 | }, 14 | "configurations": { 15 | "development": { 16 | "outputPath": "apps/demo-nextjs-page-router" 17 | }, 18 | "production": {} 19 | } 20 | }, 21 | "serve": { 22 | "executor": "@nx/next:server", 23 | "defaultConfiguration": "development", 24 | "options": { 25 | "buildTarget": "demo-nextjs-page-router:build", 26 | "dev": true 27 | }, 28 | "configurations": { 29 | "development": { 30 | "buildTarget": "demo-nextjs-page-router:build:development", 31 | "dev": true 32 | }, 33 | "production": { 34 | "buildTarget": "demo-nextjs-page-router:build:production", 35 | "dev": false 36 | } 37 | } 38 | }, 39 | "export": { 40 | "executor": "@nx/next:export", 41 | "options": { 42 | "buildTarget": "demo-nextjs-page-router:build:production" 43 | } 44 | }, 45 | "test": { 46 | "executor": "@nx/jest:jest", 47 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 48 | "options": { 49 | "jestConfig": "apps/demo-nextjs-page-router/jest.config.ts", 50 | "passWithNoTests": true 51 | } 52 | }, 53 | "lint": { 54 | "executor": "@nx/linter:eslint", 55 | "outputs": ["{options.outputFile}"], 56 | "options": { 57 | "lintFilePatterns": [ 58 | "apps/demo-nextjs-page-router/**/*.{ts,tsx,js,jsx}" 59 | ] 60 | } 61 | } 62 | }, 63 | "tags": [] 64 | } 65 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/demo-nextjs-page-router/public/.gitkeep -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/public/placeholder@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/apps/demo-nextjs-page-router/public/placeholder@2x.jpg -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import "@inrupt/jest-jsdom-polyfills"; 2 | 3 | import { render } from "@testing-library/react"; 4 | 5 | import Index from "../pages/index"; 6 | 7 | describe("Index", () => { 8 | xit("should render successfully", () => { 9 | const { baseElement } = render(); 10 | expect(baseElement).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require("@nx/react/tailwind"); 2 | const { join } = require("path"); 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | content: [ 7 | join( 8 | __dirname, 9 | "{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}", 10 | ), 11 | ...createGlobPatternsForDependencies(__dirname), 12 | ], 13 | darkMode: "class", 14 | theme: { 15 | extend: {}, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "types": ["jest", "node"] 15 | }, 16 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], 17 | "exclude": ["node_modules", "jest.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/demo-nextjs-page-router/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "**/*.test.ts", 12 | "**/*.spec.ts", 13 | "**/*.test.tsx", 14 | "**/*.spec.tsx", 15 | "**/*.test.js", 16 | "**/*.spec.js", 17 | "**/*.test.jsx", 18 | "**/*.spec.jsx", 19 | "**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /cspell-dictionary.txt: -------------------------------------------------------------------------------- 1 | quickstart 2 | runtimes 3 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "language": "en", 5 | "languageId": "markdown", 6 | "dictionaryDefinitions": [ 7 | { 8 | "name": "project-words", 9 | "path": "./cspell-dictionary.txt", 10 | "addWords": true 11 | } 12 | ], 13 | "dictionaries": ["en_US", "project-words", "typescript", "python"] 14 | } 15 | -------------------------------------------------------------------------------- /docs/reference/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/reference/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-code-background: #FFFFFF; 3 | --dark-code-background: #1E1E1E; 4 | } 5 | 6 | @media (prefers-color-scheme: light) { :root { 7 | --code-background: var(--light-code-background); 8 | } } 9 | 10 | @media (prefers-color-scheme: dark) { :root { 11 | --code-background: var(--dark-code-background); 12 | } } 13 | 14 | :root[data-theme='light'] { 15 | --code-background: var(--light-code-background); 16 | } 17 | 18 | :root[data-theme='dark'] { 19 | --code-background: var(--dark-code-background); 20 | } 21 | 22 | pre, code { background: var(--code-background); } 23 | -------------------------------------------------------------------------------- /docs/reference/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE4uOBQApu0wNAgAAAA==" -------------------------------------------------------------------------------- /docs/reference/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAAEz2MQQqDMBRE7zLr4MKuzA16ATfioiQjfDD/S0xtQby7pEp384bH25Hts8IPo4No5Bd+x8a8iik82ubRdHCYhHOsGvSVCIdgKVELHKKF92+Ot9YzFMv/5sZcGJ9Xu16LLJxFWek4TiJTfzuBAAAA"; -------------------------------------------------------------------------------- /docs/reference/index.html: -------------------------------------------------------------------------------- 1 | @fal-ai/client

@fal-ai/client

2 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from "@nx/jest"; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require("@nx/jest/preset").default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fal-ai/fal-js/a90a632174679b016298657c791088d0496827cc/libs/.gitkeep -------------------------------------------------------------------------------- /libs/client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nx/js/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/client/README.md: -------------------------------------------------------------------------------- 1 | # fal.ai JavaScript/TypeScript client library 2 | 3 | ![@fal-ai/client npm package](https://img.shields.io/npm/v/@fal-ai/client?color=%237527D7&label=%40fal-ai%2Fclient&style=flat-square) 4 | 5 | ## Introduction 6 | 7 | The `fal.ai` JavaScript Client Library provides a seamless way to interact with `fal` endpoints from your JavaScript or TypeScript applications. With built-in support for various platforms, it ensures consistent behavior across web, Node.js, and React Native environments. 8 | 9 | ## Getting started 10 | 11 | Before diving into the client-specific features, ensure you've set up your credentials: 12 | 13 | ```ts 14 | import { fal } from "@fal-ai/client"; 15 | 16 | fal.config({ 17 | // Can also be auto-configured using environment variables: 18 | // Either a single FAL_KEY or a combination of FAL_KEY_ID and FAL_KEY_SECRET 19 | credentials: "FAL_KEY_ID:FAL_KEY_SECRET", 20 | }); 21 | ``` 22 | 23 | **Note:** Ensure you've reviewed the [fal.ai getting started guide](https://fal.ai/docs) to acquire your credentials and register your functions. Also, make sure your credentials are always protected. See the [../proxy](../proxy) package for a secure way to use the client in client-side applications. 24 | 25 | ## Running functions with `fal.run` 26 | 27 | The `fal.run` method is the simplest way to execute a function. It returns a promise that resolves to the function's result: 28 | 29 | ```ts 30 | const result = await fal.run("my-function-id", { 31 | input: { foo: "bar" }, 32 | }); 33 | ``` 34 | 35 | ## Long-running functions with `fal.subscribe` 36 | 37 | The `fal.subscribe` method offers a powerful way to rely on the [queue system](https://www.fal.ai/docs/function-endpoints/queue) to execute long-running functions. It returns the result once it's done like any other async function, so your don't have to deal with queue status updates yourself. However, it does support queue events, in case you want to listen and react to them: 38 | 39 | ```ts 40 | const result = await fal.subscribe("my-function-id", { 41 | input: { foo: "bar" }, 42 | onQueueUpdate(update) { 43 | if (update.status === "IN_QUEUE") { 44 | console.log(`Your position in the queue is ${update.position}`); 45 | } 46 | }, 47 | }); 48 | ``` 49 | 50 | ## More features 51 | 52 | The client library offers a plethora of features designed to simplify your journey with `fal.ai`. Dive into the [official documentation](https://fal.ai/docs) for a comprehensive guide. 53 | -------------------------------------------------------------------------------- /libs/client/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "client", 4 | preset: "../../jest.preset.js", 5 | globals: {}, 6 | testEnvironment: "node", 7 | transform: { 8 | "^.+\\.[tj]sx?$": [ 9 | "ts-jest", 10 | { 11 | tsconfig: "/tsconfig.spec.json", 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 16 | coverageDirectory: "../../coverage/libs/client", 17 | }; 18 | -------------------------------------------------------------------------------- /libs/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fal-ai/client", 3 | "description": "The fal.ai client for JavaScript and TypeScript", 4 | "version": "1.6.2", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/fal-ai/fal-js.git", 9 | "directory": "libs/client" 10 | }, 11 | "keywords": [ 12 | "fal", 13 | "client", 14 | "ai", 15 | "ml", 16 | "typescript" 17 | ], 18 | "exports": { 19 | ".": "./src/index.js", 20 | "./endpoints": "./src/types/endpoints.js" 21 | }, 22 | "typesVersions": { 23 | "*": { 24 | "endpoints": [ 25 | "src/types/endpoints.d.ts" 26 | ] 27 | } 28 | }, 29 | "main": "./src/index.js", 30 | "types": "./src/index.d.ts", 31 | "dependencies": { 32 | "@msgpack/msgpack": "^3.0.0-beta2", 33 | "eventsource-parser": "^1.1.2", 34 | "robot3": "^0.4.1" 35 | }, 36 | "engines": { 37 | "node": ">=18.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libs/client/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/client/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/client", 12 | "tsConfig": "libs/client/tsconfig.lib.json", 13 | "packageJson": "libs/client/package.json", 14 | "main": "libs/client/src/index.ts", 15 | "assets": ["LICENSE", "CODE_OF_CONDUCT.md", "libs/client/README.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["libs/client/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 28 | "options": { 29 | "jestConfig": "libs/client/jest.config.ts", 30 | "passWithNoTests": true 31 | }, 32 | "configurations": { 33 | "ci": { 34 | "ci": true, 35 | "codeCoverage": true 36 | } 37 | } 38 | }, 39 | "release": { 40 | "executor": "@theunderscorer/nx-semantic-release:semantic-release" 41 | } 42 | }, 43 | "tags": [] 44 | } 45 | -------------------------------------------------------------------------------- /libs/client/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { getRestApiUrl, RequiredConfig } from "./config"; 2 | import { dispatchRequest } from "./request"; 3 | import { parseEndpointId } from "./utils"; 4 | 5 | export const TOKEN_EXPIRATION_SECONDS = 120; 6 | 7 | /** 8 | * Get a token to connect to the realtime endpoint. 9 | */ 10 | export async function getTemporaryAuthToken( 11 | app: string, 12 | config: RequiredConfig, 13 | ): Promise { 14 | const appId = parseEndpointId(app); 15 | const token: string | object = await dispatchRequest({ 16 | method: "POST", 17 | targetUrl: `${getRestApiUrl()}/tokens/`, 18 | config, 19 | input: { 20 | allowed_apps: [appId.alias], 21 | token_expiration: TOKEN_EXPIRATION_SECONDS, 22 | }, 23 | }); 24 | // keep this in case the response was wrapped (old versions of the proxy do that) 25 | // should be safe to remove in the future 26 | if (typeof token !== "string" && token["detail"]) { 27 | return token["detail"]; 28 | } 29 | return token; 30 | } 31 | -------------------------------------------------------------------------------- /libs/client/src/client.spec.ts: -------------------------------------------------------------------------------- 1 | import { buildUrl } from "./request"; 2 | 3 | describe("The function test suite", () => { 4 | it("should build the URL with a function username/app-alias", () => { 5 | const alias = "fal-ai/text-to-image"; 6 | const url = buildUrl(alias); 7 | expect(url).toMatch(`fal.run/${alias}`); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /libs/client/src/client.ts: -------------------------------------------------------------------------------- 1 | import { Config, createConfig } from "./config"; 2 | import { createQueueClient, QueueClient, QueueSubscribeOptions } from "./queue"; 3 | import { createRealtimeClient, RealtimeClient } from "./realtime"; 4 | import { buildUrl, dispatchRequest } from "./request"; 5 | import { resultResponseHandler } from "./response"; 6 | import { createStorageClient, StorageClient } from "./storage"; 7 | import { createStreamingClient, StreamingClient } from "./streaming"; 8 | import { EndpointType, InputType, OutputType } from "./types/client"; 9 | import { Result, RunOptions } from "./types/common"; 10 | 11 | /** 12 | * The main client type, it provides access to simple API model usage, 13 | * as well as access to the `queue` and `storage` APIs. 14 | * 15 | * @see createFalClient 16 | */ 17 | export interface FalClient { 18 | /** 19 | * The queue client to interact with the queue API. 20 | */ 21 | readonly queue: QueueClient; 22 | 23 | /** 24 | * The realtime client to interact with the realtime API 25 | * and receive updates in real-time. 26 | * @see #RealtimeClient 27 | * @see #RealtimeClient.connect 28 | */ 29 | readonly realtime: RealtimeClient; 30 | 31 | /** 32 | * The storage client to interact with the storage API. 33 | */ 34 | readonly storage: StorageClient; 35 | 36 | /** 37 | * The streaming client to interact with the streaming API. 38 | * @see #stream 39 | */ 40 | readonly streaming: StreamingClient; 41 | 42 | /** 43 | * Runs a fal endpoint identified by its `endpointId`. 44 | * 45 | * @param endpointId The endpoint id, e.g. `fal-ai/fast-sdxl`. 46 | * @param options The request options, including the input payload. 47 | * @returns A promise that resolves to the result of the request once it's completed. 48 | * 49 | * @note 50 | * We **do not recommend** this use for most use cases as it will block the client 51 | * until the response is received. Moreover, if the connection is closed before 52 | * the response is received, the request will be lost. Instead, we recommend 53 | * using the `subscribe` method for most use cases. 54 | */ 55 | run( 56 | endpointId: Id, 57 | options: RunOptions>, 58 | ): Promise>>; 59 | 60 | /** 61 | * Subscribes to updates for a specific request in the queue. 62 | * 63 | * @param endpointId - The ID of the API endpoint. 64 | * @param options - Options to configure how the request is run and how updates are received. 65 | * @returns A promise that resolves to the result of the request once it's completed. 66 | */ 67 | subscribe( 68 | endpointId: Id, 69 | options: RunOptions> & QueueSubscribeOptions, 70 | ): Promise>>; 71 | 72 | /** 73 | * Calls a fal app that supports streaming and provides a streaming-capable 74 | * object as a result, that can be used to get partial results through either 75 | * `AsyncIterator` or through an event listener. 76 | * 77 | * @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`. 78 | * @param options the request options, including the input payload. 79 | * @returns the `FalStream` instance. 80 | */ 81 | stream: StreamingClient["stream"]; 82 | } 83 | 84 | /** 85 | * Creates a new reference of the `FalClient`. 86 | * @param userConfig Optional configuration to override the default settings. 87 | * @returns a new instance of the `FalClient`. 88 | */ 89 | export function createFalClient(userConfig: Config = {}): FalClient { 90 | const config = createConfig(userConfig); 91 | const storage = createStorageClient({ config }); 92 | const queue = createQueueClient({ config, storage }); 93 | const streaming = createStreamingClient({ config, storage }); 94 | const realtime = createRealtimeClient({ config }); 95 | return { 96 | queue, 97 | realtime, 98 | storage, 99 | streaming, 100 | stream: streaming.stream, 101 | async run( 102 | endpointId: Id, 103 | options: RunOptions> = {}, 104 | ): Promise>> { 105 | const input = options.input 106 | ? await storage.transformInput(options.input) 107 | : undefined; 108 | return dispatchRequest, Result>>({ 109 | method: options.method, 110 | targetUrl: buildUrl(endpointId, options), 111 | input: input as InputType, 112 | config: { 113 | ...config, 114 | responseHandler: resultResponseHandler, 115 | }, 116 | options: { 117 | signal: options.abortSignal, 118 | retry: { 119 | maxRetries: 3, 120 | baseDelay: 500, 121 | maxDelay: 15000, 122 | }, 123 | }, 124 | }); 125 | }, 126 | subscribe: async (endpointId, options) => { 127 | const { request_id: requestId } = await queue.submit(endpointId, options); 128 | if (options.onEnqueue) { 129 | options.onEnqueue(requestId); 130 | } 131 | await queue.subscribeToStatus(endpointId, { requestId, ...options }); 132 | return queue.result(endpointId, { requestId }); 133 | }, 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /libs/client/src/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { createConfig } from "./config"; 2 | 3 | describe("The config test suite", () => { 4 | it("should set the config variables accordingly", () => { 5 | const newConfig = { 6 | credentials: "key-id:key-secret", 7 | }; 8 | const currentConfig = createConfig(newConfig); 9 | expect(currentConfig.credentials).toEqual(newConfig.credentials); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/client/src/config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | withMiddleware, 3 | withProxy, 4 | type RequestMiddleware, 5 | } from "./middleware"; 6 | import type { ResponseHandler } from "./response"; 7 | import { defaultResponseHandler } from "./response"; 8 | import { DEFAULT_RETRY_OPTIONS, type RetryOptions } from "./retry"; 9 | import { isBrowser } from "./runtime"; 10 | 11 | export type CredentialsResolver = () => string | undefined; 12 | 13 | type FetchType = typeof fetch; 14 | 15 | export function resolveDefaultFetch(): FetchType { 16 | if (typeof fetch === "undefined") { 17 | throw new Error( 18 | "Your environment does not support fetch. Please provide your own fetch implementation.", 19 | ); 20 | } 21 | return fetch; 22 | } 23 | 24 | export type Config = { 25 | /** 26 | * The credentials to use for the fal client. When using the 27 | * client in the browser, it's recommended to use a proxy server to avoid 28 | * exposing the credentials in the client's environment. 29 | * 30 | * By default it tries to use the `FAL_KEY` environment variable, when 31 | * `process.env` is defined. 32 | * 33 | * @see https://fal.ai/docs/model-endpoints/server-side 34 | * @see #suppressLocalCredentialsWarning 35 | */ 36 | credentials?: undefined | string | CredentialsResolver; 37 | /** 38 | * Suppresses the warning when the fal credentials are exposed in the 39 | * browser's environment. Make sure you understand the security implications 40 | * before enabling this option. 41 | */ 42 | suppressLocalCredentialsWarning?: boolean; 43 | /** 44 | * The URL of the proxy server to use for the client requests. The proxy 45 | * server should forward the requests to the fal api. 46 | */ 47 | proxyUrl?: string; 48 | /** 49 | * The request middleware to use for the client requests. By default it 50 | * doesn't apply any middleware. 51 | */ 52 | requestMiddleware?: RequestMiddleware; 53 | /** 54 | * The response handler to use for the client requests. By default it uses 55 | * a built-in response handler that returns the JSON response. 56 | */ 57 | responseHandler?: ResponseHandler; 58 | /** 59 | * The fetch implementation to use for the client requests. By default it uses 60 | * the global `fetch` function. 61 | */ 62 | fetch?: FetchType; 63 | /** 64 | * Retry configuration for handling transient errors like rate limiting and server errors. 65 | * When not specified, a default retry configuration is used. 66 | */ 67 | retry?: Partial; 68 | }; 69 | 70 | export type RequiredConfig = Required; 71 | 72 | /** 73 | * Checks if the required FAL environment variables are set. 74 | * 75 | * @returns `true` if the required environment variables are set, 76 | * `false` otherwise. 77 | */ 78 | function hasEnvVariables(): boolean { 79 | return ( 80 | typeof process !== "undefined" && 81 | process.env && 82 | (typeof process.env.FAL_KEY !== "undefined" || 83 | (typeof process.env.FAL_KEY_ID !== "undefined" && 84 | typeof process.env.FAL_KEY_SECRET !== "undefined")) 85 | ); 86 | } 87 | 88 | export const credentialsFromEnv: CredentialsResolver = () => { 89 | if (!hasEnvVariables()) { 90 | return undefined; 91 | } 92 | 93 | if (typeof process.env.FAL_KEY !== "undefined") { 94 | return process.env.FAL_KEY; 95 | } 96 | 97 | return process.env.FAL_KEY_ID 98 | ? `${process.env.FAL_KEY_ID}:${process.env.FAL_KEY_SECRET}` 99 | : undefined; 100 | }; 101 | 102 | const DEFAULT_CONFIG: Partial = { 103 | credentials: credentialsFromEnv, 104 | suppressLocalCredentialsWarning: false, 105 | requestMiddleware: (request) => Promise.resolve(request), 106 | responseHandler: defaultResponseHandler, 107 | retry: DEFAULT_RETRY_OPTIONS, 108 | }; 109 | 110 | /** 111 | * Configures the fal client. 112 | * 113 | * @param config the new configuration. 114 | */ 115 | export function createConfig(config: Config): RequiredConfig { 116 | let configuration = { 117 | ...DEFAULT_CONFIG, 118 | ...config, 119 | fetch: config.fetch ?? resolveDefaultFetch(), 120 | // Merge retry configuration with defaults 121 | retry: { 122 | ...DEFAULT_RETRY_OPTIONS, 123 | ...(config.retry || {}), 124 | }, 125 | } as RequiredConfig; 126 | if (config.proxyUrl) { 127 | configuration = { 128 | ...configuration, 129 | requestMiddleware: withMiddleware( 130 | configuration.requestMiddleware, 131 | withProxy({ targetUrl: config.proxyUrl }), 132 | ), 133 | }; 134 | } 135 | const { credentials: resolveCredentials, suppressLocalCredentialsWarning } = 136 | configuration; 137 | const credentials = 138 | typeof resolveCredentials === "function" 139 | ? resolveCredentials() 140 | : resolveCredentials; 141 | if (isBrowser() && credentials && !suppressLocalCredentialsWarning) { 142 | console.warn( 143 | "The fal credentials are exposed in the browser's environment. " + 144 | "That's not recommended for production use cases.", 145 | ); 146 | } 147 | return configuration; 148 | } 149 | 150 | /** 151 | * @returns the URL of the fal REST api endpoint. 152 | */ 153 | export function getRestApiUrl(): string { 154 | return "https://rest.alpha.fal.ai"; 155 | } 156 | -------------------------------------------------------------------------------- /libs/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createFalClient, type FalClient } from "./client"; 2 | import { Config } from "./config"; 3 | import { StreamOptions } from "./streaming"; 4 | import { EndpointType, InputType } from "./types/client"; 5 | import { RunOptions } from "./types/common"; 6 | 7 | export { createFalClient, type FalClient } from "./client"; 8 | export { withMiddleware, withProxy } from "./middleware"; 9 | export type { RequestMiddleware } from "./middleware"; 10 | export type { QueueClient } from "./queue"; 11 | export type { RealtimeClient } from "./realtime"; 12 | export { ApiError, ValidationError } from "./response"; 13 | export type { ResponseHandler } from "./response"; 14 | export { isRetryableError } from "./retry"; 15 | export type { RetryOptions } from "./retry"; 16 | export type { StorageClient } from "./storage"; 17 | export type { FalStream, StreamingClient } from "./streaming"; 18 | export * from "./types/common"; 19 | export type { 20 | QueueStatus, 21 | ValidationErrorInfo, 22 | WebHookResponse, 23 | } from "./types/common"; 24 | export { parseEndpointId } from "./utils"; 25 | 26 | type SingletonFalClient = { 27 | config(config: Config): void; 28 | } & FalClient; 29 | 30 | /** 31 | * Creates a singleton instance of the client. This is useful as a compatibility 32 | * layer for existing code that uses the clients version prior to 1.0.0. 33 | */ 34 | export const fal: SingletonFalClient = (function createSingletonFalClient() { 35 | let currentInstance: FalClient = createFalClient(); 36 | return { 37 | config(config: Config) { 38 | currentInstance = createFalClient(config); 39 | }, 40 | get queue() { 41 | return currentInstance.queue; 42 | }, 43 | get realtime() { 44 | return currentInstance.realtime; 45 | }, 46 | get storage() { 47 | return currentInstance.storage; 48 | }, 49 | get streaming() { 50 | return currentInstance.streaming; 51 | }, 52 | run(id: Id, options: RunOptions>) { 53 | return currentInstance.run(id, options); 54 | }, 55 | subscribe( 56 | endpointId: Id, 57 | options: RunOptions>, 58 | ) { 59 | return currentInstance.subscribe(endpointId, options); 60 | }, 61 | stream( 62 | endpointId: Id, 63 | options: StreamOptions>, 64 | ) { 65 | return currentInstance.stream(endpointId, options); 66 | }, 67 | } satisfies SingletonFalClient; 68 | })(); 69 | -------------------------------------------------------------------------------- /libs/client/src/middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A request configuration object. 3 | * 4 | * **Note:** This is a simplified version of the `RequestConfig` type from the 5 | * `fetch` API. It contains only the properties that are relevant for the 6 | * fal client. It also works around the fact that the `fetch` API `Request` 7 | * does not support mutability, its clone method has critical limitations 8 | * to our use case. 9 | */ 10 | export type RequestConfig = { 11 | url: string; 12 | method: string; 13 | headers?: Record; 14 | }; 15 | 16 | export type RequestMiddleware = ( 17 | request: RequestConfig, 18 | ) => Promise; 19 | 20 | /** 21 | * Setup a execution chain of middleware functions. 22 | * 23 | * @param middlewares one or more middleware functions. 24 | * @returns a middleware function that executes the given middlewares in order. 25 | */ 26 | export function withMiddleware( 27 | ...middlewares: RequestMiddleware[] 28 | ): RequestMiddleware { 29 | const isDefined = (middleware: RequestMiddleware): boolean => 30 | typeof middleware === "function"; 31 | 32 | return async (config: RequestConfig) => { 33 | let currentConfig = { ...config }; 34 | for (const middleware of middlewares.filter(isDefined)) { 35 | currentConfig = await middleware(currentConfig); 36 | } 37 | return currentConfig; 38 | }; 39 | } 40 | 41 | export type RequestProxyConfig = { 42 | targetUrl: string; 43 | }; 44 | 45 | export const TARGET_URL_HEADER = "x-fal-target-url"; 46 | 47 | export function withProxy(config: RequestProxyConfig): RequestMiddleware { 48 | const passthrough = (requestConfig: RequestConfig) => 49 | Promise.resolve(requestConfig); 50 | // when running on the server, we don't need to proxy the request 51 | if (typeof window === "undefined") { 52 | return passthrough; 53 | } 54 | // if x-fal-target-url is already set, we skip it 55 | return (requestConfig) => 56 | requestConfig.headers && TARGET_URL_HEADER in requestConfig 57 | ? passthrough(requestConfig) 58 | : Promise.resolve({ 59 | ...requestConfig, 60 | url: config.targetUrl, 61 | headers: { 62 | ...(requestConfig.headers || {}), 63 | [TARGET_URL_HEADER]: requestConfig.url, 64 | }, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /libs/client/src/request.ts: -------------------------------------------------------------------------------- 1 | import { RequiredConfig } from "./config"; 2 | import { ResponseHandler } from "./response"; 3 | import { 4 | calculateBackoffDelay, 5 | isRetryableError, 6 | type RetryOptions, 7 | } from "./retry"; 8 | import { getUserAgent, isBrowser } from "./runtime"; 9 | import { RunOptions, UrlOptions } from "./types/common"; 10 | import { ensureEndpointIdFormat, isValidUrl, sleep } from "./utils"; 11 | 12 | const isCloudflareWorkers = 13 | typeof navigator !== "undefined" && 14 | navigator?.userAgent === "Cloudflare-Workers"; 15 | 16 | type RequestOptions = { 17 | responseHandler?: ResponseHandler; 18 | /** 19 | * Retry configuration for this specific request. 20 | * If not specified, uses the default retry configuration from the client config. 21 | */ 22 | retry?: Partial; 23 | }; 24 | 25 | type RequestParams = { 26 | method?: string; 27 | targetUrl: string; 28 | input?: Input; 29 | config: RequiredConfig; 30 | options?: RequestOptions & RequestInit; 31 | headers?: Record; 32 | }; 33 | 34 | export async function dispatchRequest( 35 | params: RequestParams, 36 | ): Promise { 37 | const { targetUrl, input, config, options = {} } = params; 38 | const { 39 | credentials: credentialsValue, 40 | requestMiddleware, 41 | responseHandler, 42 | fetch, 43 | } = config; 44 | 45 | const retryOptions: RetryOptions = { 46 | ...config.retry, 47 | ...(options.retry || {}), 48 | } as RetryOptions; 49 | 50 | const executeRequest = async (): Promise => { 51 | const userAgent = isBrowser() ? {} : { "User-Agent": getUserAgent() }; 52 | const credentials = 53 | typeof credentialsValue === "function" 54 | ? credentialsValue() 55 | : credentialsValue; 56 | 57 | const { method, url, headers } = await requestMiddleware({ 58 | method: (params.method ?? options.method ?? "post").toUpperCase(), 59 | url: targetUrl, 60 | headers: params.headers, 61 | }); 62 | const authHeader = credentials 63 | ? { Authorization: `Key ${credentials}` } 64 | : {}; 65 | const requestHeaders = { 66 | ...authHeader, 67 | Accept: "application/json", 68 | "Content-Type": "application/json", 69 | ...userAgent, 70 | ...(headers ?? {}), 71 | } as HeadersInit; 72 | 73 | const { 74 | responseHandler: customResponseHandler, 75 | retry: _, 76 | ...requestInit 77 | } = options; 78 | const response = await fetch(url, { 79 | ...requestInit, 80 | method, 81 | headers: { 82 | ...requestHeaders, 83 | ...(requestInit.headers ?? {}), 84 | }, 85 | ...(!isCloudflareWorkers && { mode: "cors" }), 86 | signal: options.signal, 87 | body: 88 | method.toLowerCase() !== "get" && input 89 | ? JSON.stringify(input) 90 | : undefined, 91 | }); 92 | const handleResponse = customResponseHandler ?? responseHandler; 93 | return await handleResponse(response); 94 | }; 95 | 96 | let lastError: any; 97 | for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) { 98 | try { 99 | return await executeRequest(); 100 | } catch (error) { 101 | lastError = error; 102 | 103 | const shouldNotRetry = 104 | attempt === retryOptions.maxRetries || 105 | !isRetryableError(error, retryOptions.retryableStatusCodes) || 106 | options.signal?.aborted; 107 | if (shouldNotRetry) { 108 | throw error; 109 | } 110 | 111 | const delay = calculateBackoffDelay( 112 | attempt, 113 | retryOptions.baseDelay, 114 | retryOptions.maxDelay, 115 | retryOptions.backoffMultiplier, 116 | retryOptions.enableJitter, 117 | ); 118 | 119 | await sleep(delay); 120 | } 121 | } 122 | 123 | throw lastError; 124 | } 125 | 126 | /** 127 | * Builds the final url to run the function based on its `id` or alias and 128 | * a the options from `RunOptions`. 129 | * 130 | * @private 131 | * @param id the function id or alias 132 | * @param options the run options 133 | * @returns the final url to run the function 134 | */ 135 | export function buildUrl( 136 | id: string, 137 | options: RunOptions & UrlOptions = {}, 138 | ): string { 139 | const method = (options.method ?? "post").toLowerCase(); 140 | const path = (options.path ?? "").replace(/^\//, "").replace(/\/{2,}/, "/"); 141 | const input = options.input; 142 | const params = { 143 | ...(options.query || {}), 144 | ...(method === "get" ? input : {}), 145 | }; 146 | 147 | const queryParams = 148 | Object.keys(params).length > 0 149 | ? `?${new URLSearchParams(params).toString()}` 150 | : ""; 151 | 152 | // if a fal url is passed, just use it 153 | if (isValidUrl(id)) { 154 | const url = id.endsWith("/") ? id : `${id}/`; 155 | return `${url}${path}${queryParams}`; 156 | } 157 | 158 | const appId = ensureEndpointIdFormat(id); 159 | const subdomain = options.subdomain ? `${options.subdomain}.` : ""; 160 | const url = `https://${subdomain}fal.run/${appId}/${path}`; 161 | return `${url.replace(/\/$/, "")}${queryParams}`; 162 | } 163 | -------------------------------------------------------------------------------- /libs/client/src/response.ts: -------------------------------------------------------------------------------- 1 | import { RequiredConfig } from "./config"; 2 | import { Result, ValidationErrorInfo } from "./types/common"; 3 | 4 | export type ResponseHandler = (response: Response) => Promise; 5 | 6 | const REQUEST_ID_HEADER = "x-fal-request-id"; 7 | 8 | export type ResponseHandlerCreator = ( 9 | config: RequiredConfig, 10 | ) => ResponseHandler; 11 | 12 | type ApiErrorArgs = { 13 | message: string; 14 | status: number; 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | body?: any; 17 | }; 18 | 19 | export class ApiError extends Error { 20 | public readonly status: number; 21 | public readonly body: Body; 22 | constructor({ message, status, body }: ApiErrorArgs) { 23 | super(message); 24 | this.name = "ApiError"; 25 | this.status = status; 26 | this.body = body; 27 | } 28 | } 29 | 30 | type ValidationErrorBody = { 31 | detail: ValidationErrorInfo[]; 32 | }; 33 | 34 | export class ValidationError extends ApiError { 35 | constructor(args: ApiErrorArgs) { 36 | super(args); 37 | this.name = "ValidationError"; 38 | } 39 | 40 | get fieldErrors(): ValidationErrorInfo[] { 41 | // NOTE: this is a hack to support both FastAPI/Pydantic errors 42 | // and some custom 422 errors that might not be in the Pydantic format. 43 | if (typeof this.body.detail === "string") { 44 | return [ 45 | { 46 | loc: ["body"], 47 | msg: this.body.detail, 48 | type: "value_error", 49 | }, 50 | ]; 51 | } 52 | return this.body.detail || []; 53 | } 54 | 55 | getFieldErrors(field: string): ValidationErrorInfo[] { 56 | return this.fieldErrors.filter( 57 | (error) => error.loc[error.loc.length - 1] === field, 58 | ); 59 | } 60 | } 61 | 62 | export async function defaultResponseHandler( 63 | response: Response, 64 | ): Promise { 65 | const { status, statusText } = response; 66 | const contentType = response.headers.get("Content-Type") ?? ""; 67 | if (!response.ok) { 68 | if (contentType.includes("application/json")) { 69 | const body = await response.json(); 70 | const ErrorType = status === 422 ? ValidationError : ApiError; 71 | throw new ErrorType({ 72 | message: body.message || statusText, 73 | status, 74 | body, 75 | }); 76 | } 77 | throw new ApiError({ message: `HTTP ${status}: ${statusText}`, status }); 78 | } 79 | if (contentType.includes("application/json")) { 80 | return response.json() as Promise; 81 | } 82 | if (contentType.includes("text/html")) { 83 | return response.text() as Promise; 84 | } 85 | if (contentType.includes("application/octet-stream")) { 86 | return response.arrayBuffer() as Promise; 87 | } 88 | // TODO convert to either number or bool automatically 89 | return response.text() as Promise; 90 | } 91 | 92 | export async function resultResponseHandler( 93 | response: Response, 94 | ): Promise> { 95 | const data = await defaultResponseHandler(response); 96 | return { 97 | data, 98 | requestId: response.headers.get(REQUEST_ID_HEADER) || "", 99 | } satisfies Result; 100 | } 101 | -------------------------------------------------------------------------------- /libs/client/src/retry.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from "./response"; 2 | import { 3 | calculateBackoffDelay, 4 | DEFAULT_RETRY_OPTIONS, 5 | DEFAULT_RETRYABLE_STATUS_CODES, 6 | executeWithRetry, 7 | isRetryableError, 8 | type RetryOptions, 9 | } from "./retry"; 10 | 11 | const BAD_GATEWAY_ERROR = new ApiError({ message: "Bad gateway", status: 502 }); 12 | 13 | describe("Retry functionality", () => { 14 | describe("isRetryableError", () => { 15 | it("should return true for retryable status codes", () => { 16 | const error = new ApiError({ message: "Server error", status: 504 }); 17 | expect(isRetryableError(error, DEFAULT_RETRYABLE_STATUS_CODES)).toBe( 18 | true, 19 | ); 20 | }); 21 | 22 | it("should return false for non-retryable status codes", () => { 23 | const error = new ApiError({ message: "Bad request", status: 400 }); 24 | expect(isRetryableError(error, DEFAULT_RETRYABLE_STATUS_CODES)).toBe( 25 | false, 26 | ); 27 | }); 28 | 29 | it("should return false for non-ApiError errors", () => { 30 | const error = new Error("Network error"); 31 | expect(isRetryableError(error, DEFAULT_RETRYABLE_STATUS_CODES)).toBe( 32 | false, 33 | ); 34 | }); 35 | }); 36 | 37 | describe("calculateBackoffDelay", () => { 38 | it("should calculate exponential backoff correctly", () => { 39 | const delay1 = calculateBackoffDelay(0, 1000, 30000, 2, false); 40 | const delay2 = calculateBackoffDelay(1, 1000, 30000, 2, false); 41 | const delay3 = calculateBackoffDelay(2, 1000, 30000, 2, false); 42 | 43 | expect(delay1).toBe(1000); 44 | expect(delay2).toBe(2000); 45 | expect(delay3).toBe(4000); 46 | }); 47 | 48 | it("should respect max delay", () => { 49 | const delay = calculateBackoffDelay(10, 1000, 5000, 2, false); 50 | expect(delay).toBe(5000); 51 | }); 52 | 53 | it("should add jitter when enabled", () => { 54 | const delay1 = calculateBackoffDelay(1, 1000, 30000, 2, true); 55 | const delay2 = calculateBackoffDelay(1, 1000, 30000, 2, true); 56 | 57 | // With jitter, delays should be different (with high probability) 58 | // and within the expected range 59 | expect(delay1).toBeGreaterThan(1500); // base 2000 - 25% 60 | expect(delay1).toBeLessThan(2500); // base 2000 + 25% 61 | expect(delay2).toBeGreaterThan(1500); 62 | expect(delay2).toBeLessThan(2500); 63 | }); 64 | }); 65 | 66 | describe("executeWithRetry", () => { 67 | it("should return result on first success", async () => { 68 | const operation = jest.fn().mockResolvedValue("success"); 69 | const options: RetryOptions = { ...DEFAULT_RETRY_OPTIONS, maxRetries: 3 }; 70 | 71 | const { result, metrics } = await executeWithRetry(operation, options); 72 | 73 | expect(result).toBe("success"); 74 | expect(metrics.totalAttempts).toBe(1); 75 | expect(metrics.totalDelay).toBe(0); 76 | expect(operation).toHaveBeenCalledTimes(1); 77 | }); 78 | 79 | it("should retry on retryable errors", async () => { 80 | const operation = jest 81 | .fn() 82 | .mockRejectedValueOnce(BAD_GATEWAY_ERROR) 83 | .mockRejectedValueOnce(BAD_GATEWAY_ERROR) 84 | .mockResolvedValue("success"); 85 | 86 | const options: RetryOptions = { 87 | ...DEFAULT_RETRY_OPTIONS, 88 | maxRetries: 3, 89 | baseDelay: 10, // Short delay for testing 90 | enableJitter: false, 91 | }; 92 | 93 | const { result, metrics } = await executeWithRetry(operation, options); 94 | 95 | expect(result).toBe("success"); 96 | expect(metrics.totalAttempts).toBe(3); 97 | expect(metrics.totalDelay).toBe(30); // 10 + 20 = 30ms total delay 98 | expect(operation).toHaveBeenCalledTimes(3); 99 | }); 100 | 101 | it("should not retry on non-retryable errors", async () => { 102 | const error = new ApiError({ message: "Bad request", status: 400 }); 103 | const operation = jest.fn().mockRejectedValue(error); 104 | 105 | const options: RetryOptions = { ...DEFAULT_RETRY_OPTIONS, maxRetries: 3 }; 106 | 107 | await expect(executeWithRetry(operation, options)).rejects.toThrow(error); 108 | expect(operation).toHaveBeenCalledTimes(1); 109 | }); 110 | 111 | it("should exhaust retries and throw last error", async () => { 112 | const operation = jest.fn().mockRejectedValue(BAD_GATEWAY_ERROR); 113 | 114 | const options: RetryOptions = { 115 | ...DEFAULT_RETRY_OPTIONS, 116 | maxRetries: 2, 117 | baseDelay: 10, 118 | enableJitter: false, 119 | }; 120 | 121 | await expect(executeWithRetry(operation, options)).rejects.toThrow( 122 | BAD_GATEWAY_ERROR, 123 | ); 124 | expect(operation).toHaveBeenCalledTimes(3); // 1 initial + 2 retries 125 | }); 126 | 127 | it("should call onRetry callback", async () => { 128 | const operation = jest 129 | .fn() 130 | .mockRejectedValueOnce(BAD_GATEWAY_ERROR) 131 | .mockResolvedValue("success"); 132 | 133 | const onRetry = jest.fn(); 134 | const options: RetryOptions = { 135 | ...DEFAULT_RETRY_OPTIONS, 136 | maxRetries: 3, 137 | baseDelay: 10, 138 | enableJitter: false, 139 | }; 140 | 141 | await executeWithRetry(operation, options, onRetry); 142 | 143 | expect(onRetry).toHaveBeenCalledWith(1, BAD_GATEWAY_ERROR, 10); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /libs/client/src/retry.ts: -------------------------------------------------------------------------------- 1 | import { ApiError } from "./response"; 2 | import { sleep } from "./utils"; 3 | 4 | export type RetryOptions = { 5 | maxRetries: number; 6 | baseDelay: number; 7 | maxDelay: number; 8 | backoffMultiplier: number; 9 | retryableStatusCodes: number[]; 10 | enableJitter: boolean; 11 | }; 12 | 13 | /** 14 | * Base retryable status codes for most requests 15 | */ 16 | export const DEFAULT_RETRYABLE_STATUS_CODES = [429, 502, 503, 504]; 17 | 18 | export const DEFAULT_RETRY_OPTIONS: RetryOptions = { 19 | maxRetries: 3, 20 | baseDelay: 1000, 21 | maxDelay: 30000, 22 | backoffMultiplier: 2, 23 | retryableStatusCodes: DEFAULT_RETRYABLE_STATUS_CODES, 24 | enableJitter: true, 25 | }; 26 | 27 | /** 28 | * Determines if an error is retryable based on the status code 29 | */ 30 | export function isRetryableError( 31 | error: any, 32 | retryableStatusCodes: number[], 33 | ): boolean { 34 | return ( 35 | error instanceof ApiError && retryableStatusCodes.includes(error.status) 36 | ); 37 | } 38 | 39 | /** 40 | * Calculates the backoff delay for a given attempt using exponential backoff 41 | */ 42 | export function calculateBackoffDelay( 43 | attempt: number, 44 | baseDelay: number, 45 | maxDelay: number, 46 | backoffMultiplier: number, 47 | enableJitter: boolean, 48 | ): number { 49 | const exponentialDelay = Math.min( 50 | baseDelay * Math.pow(backoffMultiplier, attempt), 51 | maxDelay, 52 | ); 53 | 54 | if (enableJitter) { 55 | // Add ±25% jitter to prevent thundering herd 56 | const jitter = 0.25 * exponentialDelay * (Math.random() * 2 - 1); 57 | return Math.max(0, exponentialDelay + jitter); 58 | } 59 | 60 | return exponentialDelay; 61 | } 62 | 63 | /** 64 | * Retry metrics for tracking retry attempts 65 | */ 66 | export interface RetryMetrics { 67 | totalAttempts: number; 68 | totalDelay: number; 69 | lastError?: any; 70 | } 71 | 72 | /** 73 | * Executes an operation with retry logic and returns both result and metrics 74 | */ 75 | export async function executeWithRetry( 76 | operation: () => Promise, 77 | options: RetryOptions, 78 | onRetry?: (attempt: number, error: any, delay: number) => void, 79 | ): Promise<{ result: T; metrics: RetryMetrics }> { 80 | const metrics: RetryMetrics = { 81 | totalAttempts: 0, 82 | totalDelay: 0, 83 | }; 84 | 85 | let lastError: any; 86 | 87 | for (let attempt = 0; attempt <= options.maxRetries; attempt++) { 88 | metrics.totalAttempts++; 89 | 90 | try { 91 | const result = await operation(); 92 | return { result, metrics }; 93 | } catch (error) { 94 | lastError = error; 95 | metrics.lastError = error; 96 | 97 | if ( 98 | attempt === options.maxRetries || 99 | !isRetryableError(error, options.retryableStatusCodes) 100 | ) { 101 | throw error; 102 | } 103 | 104 | const delay = calculateBackoffDelay( 105 | attempt, 106 | options.baseDelay, 107 | options.maxDelay, 108 | options.backoffMultiplier, 109 | options.enableJitter, 110 | ); 111 | 112 | metrics.totalDelay += delay; 113 | 114 | if (onRetry) { 115 | onRetry(attempt + 1, error, delay); 116 | } 117 | 118 | await sleep(delay); 119 | } 120 | } 121 | 122 | throw lastError; 123 | } 124 | -------------------------------------------------------------------------------- /libs/client/src/runtime.spec.ts: -------------------------------------------------------------------------------- 1 | import { getUserAgent, isBrowser } from "./runtime"; 2 | 3 | describe("the runtime test suite", () => { 4 | it("should return false when calling isBrowser() on a test", () => { 5 | expect(isBrowser()).toBe(false); 6 | }); 7 | 8 | it("should return true when calling isBrowser() and window is present", () => { 9 | global.window = { 10 | document: {}, 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | } as any; 13 | expect(isBrowser()).toBe(true); 14 | }); 15 | 16 | it("should create the correct user agent identifier", () => { 17 | expect(getUserAgent()).toMatch(/@fal-ai\/client/); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /libs/client/src/runtime.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | export function isBrowser(): boolean { 4 | return ( 5 | typeof window !== "undefined" && typeof window.document !== "undefined" 6 | ); 7 | } 8 | 9 | let memoizedUserAgent: string | null = null; 10 | 11 | export function getUserAgent(): string { 12 | if (memoizedUserAgent !== null) { 13 | return memoizedUserAgent; 14 | } 15 | const packageInfo = require("../package.json"); 16 | memoizedUserAgent = `${packageInfo.name}/${packageInfo.version}`; 17 | return memoizedUserAgent; 18 | } 19 | -------------------------------------------------------------------------------- /libs/client/src/types/client.ts: -------------------------------------------------------------------------------- 1 | import { EndpointTypeMap } from "./endpoints"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-types 4 | export type EndpointType = keyof EndpointTypeMap | (string & {}); 5 | 6 | // Get input type based on endpoint ID 7 | export type InputType = T extends keyof EndpointTypeMap 8 | ? EndpointTypeMap[T]["input"] 9 | : Record; 10 | 11 | // Get output type based on endpoint ID 12 | export type OutputType = T extends keyof EndpointTypeMap 13 | ? EndpointTypeMap[T]["output"] 14 | : any; 15 | -------------------------------------------------------------------------------- /libs/client/src/types/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an API result, containing the data, 3 | * the request ID and any other relevant information. 4 | */ 5 | export type Result = { 6 | data: T; 7 | requestId: string; 8 | }; 9 | 10 | /** 11 | * The function input and other configuration when running 12 | * the function, such as the HTTP method to use. 13 | */ 14 | export type RunOptions = { 15 | /** 16 | * The function input. It will be submitted either as query params 17 | * or the body payload, depending on the `method`. 18 | */ 19 | readonly input?: Input; 20 | 21 | /** 22 | * The HTTP method, defaults to `post`; 23 | */ 24 | readonly method?: "get" | "post" | "put" | "delete" | string; 25 | 26 | /** 27 | * The abort signal to cancel the request. 28 | */ 29 | readonly abortSignal?: AbortSignal; 30 | }; 31 | 32 | export type UrlOptions = { 33 | /** 34 | * If `true`, the function will use the queue to run the function 35 | * asynchronously and return the result in a separate call. This 36 | * influences how the URL is built. 37 | */ 38 | readonly subdomain?: string; 39 | 40 | /** 41 | * The query parameters to include in the URL. 42 | */ 43 | readonly query?: Record; 44 | 45 | /** 46 | * The path to append to the function URL. 47 | */ 48 | path?: string; 49 | }; 50 | 51 | export type RequestLog = { 52 | message: string; 53 | level: "STDERR" | "STDOUT" | "ERROR" | "INFO" | "WARN" | "DEBUG"; 54 | source: "USER"; 55 | timestamp: string; // Using string to represent date-time format, but you could also use 'Date' type if you're going to construct Date objects. 56 | }; 57 | 58 | export type Metrics = { 59 | inference_time: number | null; 60 | }; 61 | 62 | interface BaseQueueStatus { 63 | status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED"; 64 | request_id: string; 65 | response_url: string; 66 | status_url: string; 67 | cancel_url: string; 68 | } 69 | 70 | export interface InQueueQueueStatus extends BaseQueueStatus { 71 | status: "IN_QUEUE"; 72 | queue_position: number; 73 | } 74 | 75 | export interface InProgressQueueStatus extends BaseQueueStatus { 76 | status: "IN_PROGRESS"; 77 | logs: RequestLog[]; 78 | } 79 | 80 | export interface CompletedQueueStatus extends BaseQueueStatus { 81 | status: "COMPLETED"; 82 | logs: RequestLog[]; 83 | metrics?: Metrics; 84 | } 85 | 86 | export type QueueStatus = 87 | | InProgressQueueStatus 88 | | CompletedQueueStatus 89 | | InQueueQueueStatus; 90 | 91 | export function isQueueStatus(obj: any): obj is QueueStatus { 92 | return obj && obj.status && obj.response_url; 93 | } 94 | 95 | export function isCompletedQueueStatus(obj: any): obj is CompletedQueueStatus { 96 | return isQueueStatus(obj) && obj.status === "COMPLETED"; 97 | } 98 | 99 | export type ValidationErrorInfo = { 100 | msg: string; 101 | loc: Array; 102 | type: string; 103 | }; 104 | 105 | /** 106 | * Represents the response from a WebHook request. 107 | * This is a union type that varies based on the `status` property. 108 | * 109 | * @template Payload - The type of the payload in the response. It defaults to `any`, 110 | * allowing for flexibility in specifying the structure of the payload. 111 | */ 112 | export type WebHookResponse = 113 | | { 114 | /** Indicates a successful response. */ 115 | status: "OK"; 116 | /** The payload of the response, structure determined by the Payload type. */ 117 | payload: Payload; 118 | /** Error is never present in a successful response. */ 119 | error: never; 120 | /** The unique identifier for the request. */ 121 | request_id: string; 122 | } 123 | | { 124 | /** Indicates an unsuccessful response. */ 125 | status: "ERROR"; 126 | /** The payload of the response, structure determined by the Payload type. */ 127 | payload: Payload; 128 | /** Description of the error that occurred. */ 129 | error: string; 130 | /** The unique identifier for the request. */ 131 | request_id: string; 132 | }; 133 | -------------------------------------------------------------------------------- /libs/client/src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { ensureEndpointIdFormat, parseEndpointId } from "./utils"; 2 | 3 | describe("The utils test suite", () => { 4 | it("shoud match a current appOwner/appId format", () => { 5 | const id = "fal-ai/fast-sdxl"; 6 | expect(ensureEndpointIdFormat(id)).toBe(id); 7 | }); 8 | 9 | it("shoud match a current appOwner/appId/path format", () => { 10 | const id = "fal-ai/fast-sdxl/image-to-image"; 11 | expect(ensureEndpointIdFormat(id)).toBe(id); 12 | }); 13 | 14 | it("should throw on an invalid app id format", () => { 15 | const id = "just-an-id"; 16 | expect(() => ensureEndpointIdFormat(id)).toThrowError(); 17 | }); 18 | 19 | it("should parse a current app id", () => { 20 | const id = "fal-ai/fast-sdxl"; 21 | const parsed = parseEndpointId(id); 22 | expect(parsed).toEqual({ 23 | owner: "fal-ai", 24 | alias: "fast-sdxl", 25 | }); 26 | }); 27 | 28 | it("should parse a current app id with path", () => { 29 | const id = "fal-ai/fast-sdxl/image-to-image"; 30 | const parsed = parseEndpointId(id); 31 | expect(parsed).toEqual({ 32 | owner: "fal-ai", 33 | alias: "fast-sdxl", 34 | path: "image-to-image", 35 | }); 36 | }); 37 | 38 | it("should parse a current app id with namespace", () => { 39 | const id = "workflows/fal-ai/fast-sdxl"; 40 | const parsed = parseEndpointId(id); 41 | expect(parsed).toEqual({ 42 | owner: "fal-ai", 43 | alias: "fast-sdxl", 44 | namespace: "workflows", 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /libs/client/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function ensureEndpointIdFormat(id: string): string { 2 | const parts = id.split("/"); 3 | if (parts.length > 1) { 4 | return id; 5 | } 6 | const [, appOwner, appId] = /^([0-9]+)-([a-zA-Z0-9-]+)$/.exec(id) || []; 7 | if (appOwner && appId) { 8 | return `${appOwner}/${appId}`; 9 | } 10 | throw new Error( 11 | `Invalid app id: ${id}. Must be in the format /`, 12 | ); 13 | } 14 | 15 | const ENDPOINT_NAMESPACES = ["workflows", "comfy"] as const; 16 | 17 | type EndpointNamespace = (typeof ENDPOINT_NAMESPACES)[number]; 18 | 19 | export type EndpointId = { 20 | readonly owner: string; 21 | readonly alias: string; 22 | readonly path?: string; 23 | readonly namespace?: EndpointNamespace; 24 | }; 25 | 26 | export function parseEndpointId(id: string): EndpointId { 27 | const normalizedId = ensureEndpointIdFormat(id); 28 | const parts = normalizedId.split("/"); 29 | if (ENDPOINT_NAMESPACES.includes(parts[0] as any)) { 30 | return { 31 | owner: parts[1], 32 | alias: parts[2], 33 | path: parts.slice(3).join("/") || undefined, 34 | namespace: parts[0] as EndpointNamespace, 35 | }; 36 | } 37 | return { 38 | owner: parts[0], 39 | alias: parts[1], 40 | path: parts.slice(2).join("/") || undefined, 41 | }; 42 | } 43 | 44 | export function isValidUrl(url: string) { 45 | try { 46 | const { host } = new URL(url); 47 | return /(fal\.(ai|run))$/.test(host); 48 | } catch (_) { 49 | return false; 50 | } 51 | } 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | export function throttle any>( 55 | func: T, 56 | limit: number, 57 | leading = false, 58 | ): (...funcArgs: Parameters) => ReturnType | void { 59 | let lastFunc: NodeJS.Timeout | null; 60 | let lastRan: number; 61 | 62 | return (...args: Parameters): ReturnType | void => { 63 | if (!lastRan && leading) { 64 | func(...args); 65 | lastRan = Date.now(); 66 | } else { 67 | if (lastFunc) { 68 | clearTimeout(lastFunc); 69 | } 70 | 71 | lastFunc = setTimeout( 72 | () => { 73 | if (Date.now() - lastRan >= limit) { 74 | func(...args); 75 | lastRan = Date.now(); 76 | } 77 | }, 78 | limit - (Date.now() - lastRan), 79 | ); 80 | } 81 | }; 82 | } 83 | 84 | let isRunningInReact: boolean | undefined; 85 | 86 | /** 87 | * Not really the most optimal way to detect if we're running in React, 88 | * but the idea here is that we can support multiple rendering engines 89 | * (starting with React), with all their peculiarities, without having 90 | * to add a dependency or creating custom integrations (e.g. custom hooks). 91 | * 92 | * Yes, a bit of magic to make things works out-of-the-box. 93 | * @returns `true` if running in React, `false` otherwise. 94 | */ 95 | export function isReact() { 96 | if (isRunningInReact === undefined) { 97 | const stack = new Error().stack; 98 | isRunningInReact = 99 | !!stack && 100 | (stack.includes("node_modules/react-dom/") || 101 | stack.includes("node_modules/next/")); 102 | } 103 | return isRunningInReact; 104 | } 105 | 106 | /** 107 | * Check if a value is a plain object. 108 | * @param value - The value to check. 109 | * @returns `true` if the value is a plain object, `false` otherwise. 110 | */ 111 | export function isPlainObject(value: any): boolean { 112 | return !!value && Object.getPrototypeOf(value) === Object.prototype; 113 | } 114 | 115 | /** 116 | * Utility function to sleep for a given number of milliseconds 117 | */ 118 | export async function sleep(ms: number): Promise { 119 | return new Promise((resolve) => setTimeout(resolve, ms)); 120 | } 121 | -------------------------------------------------------------------------------- /libs/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/client/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "inlineSources": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "importHelpers": false, 10 | "allowJs": true, 11 | "checkJs": false, 12 | "types": ["node"] 13 | }, 14 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 15 | "include": ["src/**/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /libs/client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "outDir": "../../dist/out-tsc", 6 | "module": "commonjs", 7 | "types": ["jest", "node"] 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /libs/create-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/create-app/README.md: -------------------------------------------------------------------------------- 1 | ## The fal.ai App Generator 2 | 3 | Generate a new full stack app configured with [fal.ai](https://fal.ai) proxy and models. 4 | 5 | ```sh 6 | npx @fal-ai/create-app 7 | ``` 8 | -------------------------------------------------------------------------------- /libs/create-app/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "create-app", 4 | preset: "../../jest.preset.js", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], 8 | }, 9 | moduleFileExtensions: ["ts", "js", "html"], 10 | coverageDirectory: "../../coverage/libs/create-app", 11 | }; 12 | -------------------------------------------------------------------------------- /libs/create-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fal-ai/create-app", 3 | "version": "0.1.4", 4 | "description": "The fal app generator.", 5 | "type": "module", 6 | "license": "MIT", 7 | "main": "src/index.js", 8 | "bin": "src/index.js", 9 | "keywords": [ 10 | "fal", 11 | "next", 12 | "nextjs", 13 | "express" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/fal-ai/fal-js.git", 18 | "directory": "libs/create-app" 19 | }, 20 | "dependencies": { 21 | "@inquirer/prompts": "^3.3.0", 22 | "@inquirer/select": "^1.3.1", 23 | "chalk": "^5.3.0", 24 | "commander": "^11.1.0", 25 | "execa": "^8.0.1", 26 | "open": "^10.0.3", 27 | "ora": "^8.0.1", 28 | "tslib": "^2.3.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libs/create-app/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-app", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/create-app/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/create-app", 12 | "tsConfig": "libs/create-app/tsconfig.lib.json", 13 | "packageJson": "libs/create-app/package.json", 14 | "main": "libs/create-app/src/index.ts", 15 | "assets": ["LICENSE", "CODE_OF_CONDUCT.md", "libs/create-app/README.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["libs/create-app/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 28 | "options": { 29 | "jestConfig": "libs/create-app/jest.config.ts", 30 | "passWithNoTests": true 31 | }, 32 | "configurations": { 33 | "ci": { 34 | "ci": true, 35 | "codeCoverage": true 36 | } 37 | } 38 | } 39 | }, 40 | "tags": [] 41 | } 42 | -------------------------------------------------------------------------------- /libs/create-app/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { input } from "@inquirer/prompts"; 4 | import select from "@inquirer/select"; 5 | import chalk from "chalk"; 6 | import childProcess from "child_process"; 7 | import { Command } from "commander"; 8 | import { execa, execaCommand } from "execa"; 9 | import fs from "fs"; 10 | import open from "open"; 11 | import ora from "ora"; 12 | import path from "path"; 13 | 14 | const program = new Command(); 15 | const log = console.log; 16 | const repoUrl = "https://github.com/fal-ai/fal-nextjs-template.git"; 17 | const green = chalk.green; 18 | const purple = chalk.hex("#6e40c9"); 19 | 20 | async function main() { 21 | const spinner = ora({ 22 | text: "Creating codebase", 23 | }); 24 | try { 25 | const kebabRegez = /^([a-z]+)(-[a-z0-9]+)*$/; 26 | 27 | program 28 | .name("The fal.ai App Generator") 29 | .description("Generate full stack AI apps integrated with fal.ai"); 30 | 31 | program.parse(process.argv); 32 | 33 | const args = program.args; 34 | let appName = args[0]; 35 | 36 | if (!appName || !kebabRegez.test(args[0])) { 37 | appName = await input({ 38 | message: "Enter your app name", 39 | default: "model-playground", 40 | validate: (d) => { 41 | if (!kebabRegez.test(d)) { 42 | return "please enter your app name in the format of my-app-name"; 43 | } 44 | return true; 45 | }, 46 | }); 47 | } 48 | 49 | const hasFalEnv = await select({ 50 | message: "Do you have a fal.ai API key?", 51 | choices: [ 52 | { 53 | name: "Yes", 54 | value: true, 55 | }, 56 | { 57 | name: "No", 58 | value: false, 59 | }, 60 | ], 61 | }); 62 | 63 | if (!hasFalEnv) { 64 | await open("https://www.fal.ai/dashboard"); 65 | } 66 | 67 | const fal_api_key = await input({ message: "Fal AI API Key" }); 68 | 69 | const envs = ` 70 | # environment, either PRODUCTION or DEVELOPMENT 71 | ENVIRONMENT="PRODUCTION" 72 | 73 | # FAL AI API Key 74 | FAL_KEY="${fal_api_key}" 75 | `; 76 | 77 | log(`\nInitializing project. \n`); 78 | 79 | spinner.start(); 80 | await execa("git", ["clone", repoUrl, appName]); 81 | 82 | let packageJson = fs.readFileSync(`${appName}/package.json`, "utf8"); 83 | const packageObj = JSON.parse(packageJson); 84 | packageObj.name = appName; 85 | packageJson = JSON.stringify(packageObj, null, 2); 86 | fs.writeFileSync(`${appName}/package.json`, packageJson); 87 | fs.writeFileSync(`${appName}/.env.local`, envs); 88 | 89 | process.chdir(path.join(process.cwd(), appName)); 90 | await execa("rm", ["-rf", ".git"]); 91 | await execa("git", ["init"]); 92 | 93 | spinner.text = ""; 94 | let startCommand = ""; 95 | 96 | if (isBunInstalled()) { 97 | spinner.text = "Installing dependencies"; 98 | await execaCommand("bun install").pipeStdout(process.stdout); 99 | spinner.text = ""; 100 | startCommand = "bun dev"; 101 | console.log("\n"); 102 | } else if (isYarnInstalled()) { 103 | await execaCommand("yarn").pipeStdout(process.stdout); 104 | startCommand = "yarn dev"; 105 | } else { 106 | spinner.text = "Installing dependencies"; 107 | await execa("npm", ["install", "--verbose"]).pipeStdout(process.stdout); 108 | spinner.text = ""; 109 | startCommand = "npm run dev"; 110 | } 111 | 112 | spinner.stop(); 113 | await execa("git", ["add", "."]); 114 | await execa("git", ["commit", "-m", "Initial commit"]); 115 | 116 | process.chdir("../"); 117 | log( 118 | `${green.bold("Success!")} Created ${purple.bold( 119 | appName, 120 | )} at ${process.cwd()} \n`, 121 | ); 122 | log( 123 | `To get started, change into the new directory and run ${chalk.cyan( 124 | startCommand, 125 | )}\n`, 126 | ); 127 | } catch (err) { 128 | log("\n"); 129 | if (err.exitCode == 128) { 130 | log("Error: directory already exists."); 131 | } 132 | spinner.stop(); 133 | } 134 | } 135 | 136 | main(); 137 | 138 | function isYarnInstalled() { 139 | try { 140 | childProcess.execSync("yarn --version"); 141 | return true; 142 | } catch { 143 | return false; 144 | } 145 | } 146 | 147 | function isBunInstalled() { 148 | try { 149 | childProcess.execSync("bun --version"); 150 | return true; 151 | } catch (err) { 152 | return false; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /libs/create-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/create-app/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "target": "es6", 8 | "noImplicitAny": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "outDir": "dist", 12 | "baseUrl": ".", 13 | "paths": { 14 | "*": ["node_modules/*", "src/types/*"] 15 | } 16 | }, 17 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 18 | "include": ["src/**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/create-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /libs/proxy/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nx/js/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/proxy/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/proxy/README.md: -------------------------------------------------------------------------------- 1 | # fal.ai proxy library 2 | 3 | ![@fal-ai/server-proxy npm package](https://img.shields.io/npm/v/@fal-ai/server-proxy?color=%237527D7&label=%40fal-ai%2Fserver-proxy&style=flat-square) 4 | 5 | ## Introduction 6 | 7 | The `@fal-ai/server-proxy` library enables you to route client requests through your own server, therefore safeguarding sensitive credentials. With built-in support for popular frameworks like Next.js and Express, setting up the proxy becomes a breeze. 8 | 9 | ### Install the proxy library: 10 | 11 | ``` 12 | npm install --save @fal-ai/server-proxy 13 | ``` 14 | 15 | ## Next.js page router integration 16 | 17 | For Next.js applications using the page router: 18 | 19 | 1. Create an API route in your Next.js app, as a convention we suggest using `pages/api/fal/proxy.js` (or `.ts` if you're using TypeScript): 20 | 2. Re-export the proxy handler from the library as the default export: 21 | ```ts 22 | export { handler as default } from "@fal-ai/server-proxy/nextjs"; 23 | ``` 24 | 3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key. 25 | 26 | ## Next.js app router integration 27 | 28 | For Next.js applications using the app router: 29 | 30 | 1. Create an API route in your Next.js app, as a convention we suggest using `app/api/fal/proxy/route.js` (or `.ts` if you're using TypeScript): 31 | 2. Re-export the proxy handler from the library as the default export: 32 | 33 | ```ts 34 | import { route } from "@fal-ai/server-proxy/nextjs"; 35 | 36 | export const { GET, POST, PUT } = route; 37 | ``` 38 | 39 | 3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key. 40 | 41 | ## Express integration 42 | 43 | For Express applications: 44 | 45 | 1. Make sure your app supports JSON payloads, either by using `express.json()` (recommended) or `body-parser`: 46 | ```ts 47 | app.use(express.json()); 48 | ``` 49 | 2. Add the proxy route and its handler. Note that if your client lives outside of the express app (i.e. the express app is solely used as an external API for other clients), you will need to allow CORS on the proxy route: 50 | 51 | ```ts 52 | import * as falProxy from "@fal-ai/server-proxy/express"; 53 | 54 | app.all( 55 | falProxy.route, // '/api/fal/proxy' or you can use your own 56 | cors(), // if external clients will use the proxy 57 | falProxy.handler, 58 | ); 59 | ``` 60 | 61 | 3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key. 62 | 63 | ## Client configuration 64 | 65 | Once you've set up the proxy, you can configure the client to use it: 66 | 67 | ```ts 68 | import { fal } from "@fal-ai/client"; 69 | 70 | fal.config({ 71 | proxyUrl: "/api/fal/proxy", // or https://my.app.com/api/fal/proxy 72 | }); 73 | ``` 74 | 75 | Now all your client calls will route through your server proxy, so your credentials are protected. 76 | 77 | ## More information 78 | 79 | For a deeper dive into the proxy library and its capabilities, explore the [official documentation](https://fal.ai/docs). 80 | -------------------------------------------------------------------------------- /libs/proxy/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: "proxy", 4 | preset: "../../jest.preset.js", 5 | globals: {}, 6 | testEnvironment: "node", 7 | transform: { 8 | "^.+\\.[tj]sx?$": [ 9 | "ts-jest", 10 | { 11 | tsconfig: "/tsconfig.spec.json", 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 16 | coverageDirectory: "../../coverage/libs/proxy", 17 | }; 18 | -------------------------------------------------------------------------------- /libs/proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fal-ai/server-proxy", 3 | "description": "The fal.ai server proxy adapter for JavaScript and TypeScript Web frameworks", 4 | "version": "1.1.1", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/fal-ai/fal-js.git", 9 | "directory": "libs/proxy" 10 | }, 11 | "keywords": [ 12 | "fal", 13 | "client", 14 | "next", 15 | "nextjs", 16 | "express", 17 | "hono", 18 | "proxy" 19 | ], 20 | "exports": { 21 | ".": "./src/index.js", 22 | "./express": "./src/express.js", 23 | "./hono": "./src/hono.js", 24 | "./nextjs": "./src/nextjs.js", 25 | "./remix": "./src/remix.js", 26 | "./svelte": "./src/svelte.js" 27 | }, 28 | "typesVersions": { 29 | "*": { 30 | "express": [ 31 | "src/express.d.ts" 32 | ], 33 | "hono": [ 34 | "src/hono.d.ts" 35 | ], 36 | "nextjs": [ 37 | "src/nextjs.d.ts" 38 | ], 39 | "remix": [ 40 | "src/remix.d.ts" 41 | ], 42 | "svelte": [ 43 | "src/svelte.d.ts" 44 | ] 45 | } 46 | }, 47 | "main": "./src/index.js", 48 | "types": "./src/index.d.ts", 49 | "peerDependencies": { 50 | "@remix-run/dev": "^2.0.0", 51 | "@sveltejs/kit": "^2.0.0", 52 | "express": "^4.0.0", 53 | "hono": "^4.0.0", 54 | "next": "13.4 - 14 || >=15.0.0-0", 55 | "react": "^18.0.0 || >=19.0.0-0", 56 | "react-dom": "^18.0.0 || >=19.0.0-0" 57 | }, 58 | "peerDependenciesMeta": { 59 | "@remix-run/dev": { 60 | "optional": true 61 | }, 62 | "@sveltejs/kit": { 63 | "optional": true 64 | }, 65 | "express": { 66 | "optional": true 67 | }, 68 | "hono": { 69 | "optional": true 70 | }, 71 | "next": { 72 | "optional": true 73 | }, 74 | "react": { 75 | "optional": true 76 | }, 77 | "react-dom": { 78 | "optional": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /libs/proxy/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/proxy/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/proxy", 12 | "tsConfig": "libs/proxy/tsconfig.lib.json", 13 | "packageJson": "libs/proxy/package.json", 14 | "main": "libs/proxy/src/index.ts", 15 | "assets": ["LICENSE", "CODE_OF_CONDUCT.md", "libs/proxy/README.md"] 16 | } 17 | }, 18 | "lint": { 19 | "executor": "@nx/linter:eslint", 20 | "outputs": ["{options.outputFile}"], 21 | "options": { 22 | "lintFilePatterns": ["libs/proxy/**/*.ts"] 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nx/jest:jest", 27 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 28 | "options": { 29 | "jestConfig": "libs/proxy/jest.config.ts", 30 | "passWithNoTests": true 31 | }, 32 | "configurations": { 33 | "ci": { 34 | "ci": true, 35 | "codeCoverage": true 36 | } 37 | } 38 | } 39 | }, 40 | "tags": [] 41 | } 42 | -------------------------------------------------------------------------------- /libs/proxy/src/express.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { DEFAULT_PROXY_ROUTE, handleRequest } from "./index"; 3 | 4 | /** 5 | * The default Express route for the fal.ai client proxy. 6 | */ 7 | export const route = DEFAULT_PROXY_ROUTE; 8 | 9 | /** 10 | * The Express route handler for the fal.ai client proxy. 11 | * 12 | * @param request The Express request object. 13 | * @param response The Express response object. 14 | * @param next The Express next function. 15 | */ 16 | export const handler: RequestHandler = async (request, response, next) => { 17 | await handleRequest({ 18 | id: "express", 19 | method: request.method, 20 | getRequestBody: async () => JSON.stringify(request.body), 21 | getHeaders: () => request.headers, 22 | getHeader: (name) => request.headers[name], 23 | sendHeader: (name, value) => response.setHeader(name, value), 24 | respondWith: (status, data) => response.status(status).json(data), 25 | sendResponse: async (res) => { 26 | if (res.body instanceof ReadableStream) { 27 | const reader = res.body.getReader(); 28 | const stream = async () => { 29 | const { done, value } = await reader.read(); 30 | if (done) { 31 | response.end(); 32 | return response; 33 | } 34 | response.write(value); 35 | return await stream(); 36 | }; 37 | 38 | return await stream().catch((error) => { 39 | if (!response.headersSent) { 40 | response.status(500).send(error.message); 41 | } else { 42 | response.end(); 43 | } 44 | }); 45 | } 46 | if (res.headers.get("content-type")?.includes("application/json")) { 47 | return response.status(res.status).json(await res.json()); 48 | } 49 | return response.status(res.status).send(await res.text()); 50 | }, 51 | }); 52 | next(); 53 | }; 54 | -------------------------------------------------------------------------------- /libs/proxy/src/hono.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "hono"; 2 | import { type StatusCode } from "hono/utils/http-status"; 3 | import { 4 | handleRequest, 5 | HeaderValue, 6 | resolveApiKeyFromEnv, 7 | responsePassthrough, 8 | } from "./index"; 9 | 10 | export type FalHonoProxyOptions = { 11 | /** 12 | * A function to resolve the API key used by the proxy. 13 | * By default, it uses the `FAL_KEY` environment variable. 14 | */ 15 | resolveApiKey?: () => Promise; 16 | }; 17 | 18 | type RouteHandler = (context: Context) => Promise; 19 | 20 | /** 21 | * Creates a route handler that proxies requests to the fal API. 22 | * 23 | * This is a drop-in handler for Hono applications so that the client can be called 24 | * directly from the client-side code while keeping API keys safe. 25 | * 26 | * @param param the proxy options. 27 | * @returns a Hono route handler function. 28 | */ 29 | export function createRouteHandler({ 30 | resolveApiKey = resolveApiKeyFromEnv, 31 | }: FalHonoProxyOptions): RouteHandler { 32 | const routeHandler: RouteHandler = async (context) => { 33 | const responseHeaders: Record = {}; 34 | const response = await handleRequest({ 35 | id: "hono", 36 | method: context.req.method, 37 | respondWith: (status, data) => { 38 | return context.json(data, status as StatusCode, responseHeaders); 39 | }, 40 | getHeaders: () => responseHeaders, 41 | getHeader: (name) => context.req.header(name), 42 | sendHeader: (name, value) => (responseHeaders[name] = value), 43 | getRequestBody: async () => JSON.stringify(await context.req.json()), 44 | sendResponse: responsePassthrough, 45 | resolveApiKey, 46 | }); 47 | return response; 48 | }; 49 | 50 | return routeHandler; 51 | } 52 | -------------------------------------------------------------------------------- /libs/proxy/src/index.ts: -------------------------------------------------------------------------------- 1 | export const TARGET_URL_HEADER = "x-fal-target-url"; 2 | 3 | export const DEFAULT_PROXY_ROUTE = "/api/fal/proxy"; 4 | 5 | const FAL_KEY = process.env.FAL_KEY; 6 | const FAL_KEY_ID = process.env.FAL_KEY_ID; 7 | const FAL_KEY_SECRET = process.env.FAL_KEY_SECRET; 8 | 9 | export type HeaderValue = string | string[] | undefined | null; 10 | 11 | const FAL_URL_REG_EXP = /(\.|^)fal\.(run|ai)$/; 12 | 13 | /** 14 | * The proxy behavior that is passed to the proxy handler. This is a subset of 15 | * request objects that are used by different frameworks, like Express and NextJS. 16 | */ 17 | export interface ProxyBehavior { 18 | id: string; 19 | method: string; 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | respondWith(status: number, data: string | any): ResponseType; 22 | sendResponse(response: Response): Promise; 23 | getHeaders(): Record; 24 | getHeader(name: string): HeaderValue; 25 | sendHeader(name: string, value: string): void; 26 | getRequestBody(): Promise; 27 | resolveApiKey?: () => Promise; 28 | } 29 | 30 | /** 31 | * Utility to get a header value as `string` from a Headers object. 32 | * 33 | * @private 34 | * @param request the header value. 35 | * @returns the header value as `string` or `undefined` if the header is not set. 36 | */ 37 | function singleHeaderValue(value: HeaderValue): string | undefined { 38 | if (!value) { 39 | return undefined; 40 | } 41 | if (Array.isArray(value)) { 42 | return value[0]; 43 | } 44 | return value; 45 | } 46 | 47 | function getFalKey(): string | undefined { 48 | if (FAL_KEY) { 49 | return FAL_KEY; 50 | } 51 | if (FAL_KEY_ID && FAL_KEY_SECRET) { 52 | return `${FAL_KEY_ID}:${FAL_KEY_SECRET}`; 53 | } 54 | return undefined; 55 | } 56 | 57 | const EXCLUDED_HEADERS = ["content-length", "content-encoding"]; 58 | 59 | /** 60 | * A request handler that proxies the request to the fal API 61 | * endpoint. This is useful so client-side calls to the fal endpoint 62 | * can be made without CORS issues and the correct credentials can be added 63 | * effortlessly. 64 | * 65 | * @param behavior the request proxy behavior. 66 | * @returns Promise the promise that will be resolved once the request is done. 67 | */ 68 | export async function handleRequest( 69 | behavior: ProxyBehavior, 70 | ) { 71 | const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER)); 72 | if (!targetUrl) { 73 | return behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`); 74 | } 75 | 76 | const urlHost = new URL(targetUrl).host; 77 | if (!FAL_URL_REG_EXP.test(urlHost)) { 78 | return behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`); 79 | } 80 | 81 | const falKey = behavior.resolveApiKey 82 | ? await behavior.resolveApiKey() 83 | : getFalKey(); 84 | if (!falKey) { 85 | return behavior.respondWith(401, "Missing fal.ai credentials"); 86 | } 87 | 88 | // pass over headers prefixed with x-fal-* 89 | const headers: Record = {}; 90 | Object.keys(behavior.getHeaders()).forEach((key) => { 91 | if (key.toLowerCase().startsWith("x-fal-")) { 92 | headers[key.toLowerCase()] = behavior.getHeader(key); 93 | } 94 | }); 95 | 96 | const proxyUserAgent = `@fal-ai/server-proxy/${behavior.id}`; 97 | const userAgent = singleHeaderValue(behavior.getHeader("user-agent")); 98 | const res = await fetch(targetUrl, { 99 | method: behavior.method, 100 | headers: { 101 | ...headers, 102 | authorization: 103 | singleHeaderValue(behavior.getHeader("authorization")) ?? 104 | `Key ${falKey}`, 105 | accept: "application/json", 106 | "content-type": "application/json", 107 | "user-agent": userAgent, 108 | "x-fal-client-proxy": proxyUserAgent, 109 | } as HeadersInit, 110 | body: 111 | behavior.method?.toUpperCase() === "GET" 112 | ? undefined 113 | : await behavior.getRequestBody(), 114 | }); 115 | 116 | // copy headers from fal to the proxied response 117 | res.headers.forEach((value, key) => { 118 | if (!EXCLUDED_HEADERS.includes(key.toLowerCase())) { 119 | behavior.sendHeader(key, value); 120 | } 121 | }); 122 | 123 | return behavior.sendResponse(res); 124 | } 125 | 126 | export function fromHeaders( 127 | headers: Headers, 128 | ): Record { 129 | // TODO once Header.entries() is available, use that instead 130 | // Object.fromEntries(headers.entries()); 131 | const result: Record = {}; 132 | headers.forEach((value, key) => { 133 | result[key] = value; 134 | }); 135 | return result; 136 | } 137 | 138 | export const responsePassthrough = (res: Response) => Promise.resolve(res); 139 | 140 | export const resolveApiKeyFromEnv = () => Promise.resolve(getFalKey()); 141 | -------------------------------------------------------------------------------- /libs/proxy/src/nextjs.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, type NextRequest } from "next/server"; 2 | import type { NextApiHandler } from "next/types"; 3 | import { 4 | DEFAULT_PROXY_ROUTE, 5 | fromHeaders, 6 | handleRequest, 7 | responsePassthrough, 8 | } from "./index"; 9 | 10 | /** 11 | * The default Next API route for the fal.ai client proxy. 12 | */ 13 | export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE; 14 | 15 | /** 16 | * The Next API route handler for the fal.ai client proxy. 17 | * Use it with the /pages router in Next.js. 18 | * 19 | * Note: the page routers proxy doesn't support streaming responses. 20 | * 21 | * @param request the Next API request object. 22 | * @param response the Next API response object. 23 | * @returns a promise that resolves when the request is handled. 24 | */ 25 | export const handler: NextApiHandler = async (request, response) => { 26 | return handleRequest({ 27 | id: "nextjs-page-router", 28 | method: request.method || "POST", 29 | getRequestBody: async () => JSON.stringify(request.body), 30 | getHeaders: () => request.headers, 31 | getHeader: (name) => request.headers[name], 32 | sendHeader: (name, value) => response.setHeader(name, value), 33 | respondWith: (status, data) => response.status(status).json(data), 34 | sendResponse: async (res) => { 35 | if (res.headers.get("content-type")?.includes("application/json")) { 36 | return response.status(res.status).json(await res.json()); 37 | } 38 | return response.status(res.status).send(await res.text()); 39 | }, 40 | }); 41 | }; 42 | 43 | /** 44 | * The Next API route handler for the fal.ai client proxy on App Router apps. 45 | * 46 | * @param request the Next API request object. 47 | * @returns a promise that resolves when the request is handled. 48 | */ 49 | async function routeHandler(request: NextRequest) { 50 | const responseHeaders = new Headers(); 51 | return await handleRequest({ 52 | id: "nextjs-app-router", 53 | method: request.method, 54 | getRequestBody: async () => request.text(), 55 | getHeaders: () => fromHeaders(request.headers), 56 | getHeader: (name) => request.headers.get(name), 57 | sendHeader: (name, value) => responseHeaders.set(name, value), 58 | respondWith: (status, data) => 59 | NextResponse.json(data, { 60 | status, 61 | headers: responseHeaders, 62 | }), 63 | sendResponse: responsePassthrough, 64 | }); 65 | } 66 | 67 | export const route = { 68 | handler: routeHandler, 69 | GET: routeHandler, 70 | POST: routeHandler, 71 | PUT: routeHandler, 72 | }; 73 | -------------------------------------------------------------------------------- /libs/proxy/src/remix.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunction, 3 | ActionFunctionArgs, 4 | LoaderFunction, 5 | LoaderFunctionArgs, 6 | json as jsonFunction, 7 | } from "@remix-run/node"; 8 | import { 9 | fromHeaders, 10 | handleRequest, 11 | resolveApiKeyFromEnv, 12 | responsePassthrough, 13 | } from "./index"; 14 | 15 | export type FalRemixProxy = { 16 | action: ActionFunction; 17 | loader: LoaderFunction; 18 | }; 19 | 20 | export type FalRemixProxyOptions = { 21 | /** 22 | * The reference to the `json` function from the Remix runtime. 23 | * e.g. `import { json } from "@remix-run/node";` 24 | */ 25 | json: typeof jsonFunction; 26 | /** 27 | * A function to resolve the API key used by the proxy. 28 | * By default, it uses the `FAL_KEY` environment variable. 29 | */ 30 | resolveApiKey?: () => Promise; 31 | }; 32 | 33 | export function createProxy({ 34 | json, 35 | resolveApiKey = resolveApiKeyFromEnv, 36 | }: FalRemixProxyOptions): FalRemixProxy { 37 | const proxy = async ({ 38 | request, 39 | }: ActionFunctionArgs | LoaderFunctionArgs) => { 40 | const responseHeaders = new Headers(); 41 | return handleRequest({ 42 | id: "remix", 43 | method: request.method, 44 | respondWith: (status, data) => 45 | json(data, { status, headers: responseHeaders }), 46 | getHeaders: () => fromHeaders(request.headers), 47 | getHeader: (name) => request.headers.get(name), 48 | sendHeader: (name, value) => responseHeaders.set(name, value), 49 | getRequestBody: async () => JSON.stringify(await request.json()), 50 | sendResponse: responsePassthrough, 51 | resolveApiKey, 52 | }); 53 | }; 54 | return { 55 | action: proxy, 56 | loader: proxy, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /libs/proxy/src/svelte.ts: -------------------------------------------------------------------------------- 1 | import { type RequestHandler } from "@sveltejs/kit"; 2 | import { fromHeaders, handleRequest } from "./index"; 3 | 4 | type RequestHandlerParams = { 5 | /** 6 | * The credentials to use for the request. Usually comes from `$env/static/private` 7 | */ 8 | credentials?: string | undefined; 9 | }; 10 | 11 | /** 12 | * Creates the SvelteKit request handler for the fal.ai client proxy on App Router apps. 13 | * The passed credentials will be used to authenticate the request, if not provided the 14 | * environment variable `FAL_KEY` will be used. 15 | * 16 | * @param params the request handler parameters. 17 | * @returns the SvelteKit request handler. 18 | */ 19 | export const createRequestHandler = ({ 20 | credentials, 21 | }: RequestHandlerParams = {}) => { 22 | const handler: RequestHandler = async ({ request }) => { 23 | const FAL_KEY = credentials || process.env.FAL_KEY || ""; 24 | const responseHeaders = new Headers({ 25 | "Content-Type": "application/json", 26 | }); 27 | return await handleRequest({ 28 | id: "svelte-app-router", 29 | method: request.method, 30 | getRequestBody: async () => request.text(), 31 | getHeaders: () => fromHeaders(request.headers), 32 | getHeader: (name) => request.headers.get(name), 33 | sendHeader: (name, value) => (responseHeaders[name] = value), 34 | resolveApiKey: () => Promise.resolve(FAL_KEY), 35 | respondWith: (status, data) => 36 | new Response(JSON.stringify(data), { 37 | status, 38 | headers: responseHeaders, 39 | }), 40 | sendResponse: async (res) => { 41 | return new Response(res.body, res); 42 | }, 43 | }); 44 | }; 45 | return { 46 | requestHandler: handler, 47 | GET: handler, 48 | POST: handler, 49 | PUT: handler, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /libs/proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "compilerOptions": { 6 | "target": "ES2020", 7 | "importHelpers": false 8 | }, 9 | "references": [ 10 | { 11 | "path": "./tsconfig.lib.json" 12 | }, 13 | { 14 | "path": "./tsconfig.spec.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/proxy/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "types": ["node"] 9 | }, 10 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/proxy/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.test.tsx", 13 | "src/**/*.spec.tsx", 14 | "src/**/*.test.js", 15 | "src/**/*.spec.js", 16 | "src/**/*.test.jsx", 17 | "src/**/*.spec.jsx", 18 | "src/**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "extends": "nx/presets/npm.json", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "nx-cloud", 7 | "options": { 8 | "cacheableOperations": ["build", "lint", "test", "e2e"], 9 | "accessToken": "" 10 | } 11 | } 12 | }, 13 | "neverConnectToCloud": true, 14 | "targetDefaults": { 15 | "build": { 16 | "dependsOn": ["^build"], 17 | "inputs": ["production", "^production"] 18 | }, 19 | "test": { 20 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 21 | }, 22 | "e2e": { 23 | "inputs": ["default", "^production"] 24 | }, 25 | "lint": { 26 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 27 | } 28 | }, 29 | "namedInputs": { 30 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 31 | "production": [ 32 | "default", 33 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 34 | "!{projectRoot}/tsconfig.spec.json", 35 | "!{projectRoot}/jest.config.[jt]s", 36 | "!{projectRoot}/.eslintrc.json", 37 | "!{projectRoot}/src/test-setup.[jt]s" 38 | ], 39 | "sharedGlobals": ["{workspaceRoot}/babel.config.json"] 40 | }, 41 | "generators": { 42 | "@nx/next": { 43 | "application": { 44 | "style": "css", 45 | "linter": "eslint" 46 | } 47 | }, 48 | "@nx/react": { 49 | "application": { 50 | "babel": true 51 | } 52 | } 53 | }, 54 | "defaultProject": "demo-nextjs-app-router" 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fal-ai/fal-js", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve", 7 | "build": "nx build", 8 | "test": "nx test", 9 | "lint:staged": "lint-staged", 10 | "docs:typedoc": "typedoc --tsconfig libs/client/tsconfig.lib.json", 11 | "prepare": "husky install" 12 | }, 13 | "private": true, 14 | "lint-staged": { 15 | ".{env,env.example}": [ 16 | "secretlint" 17 | ], 18 | "*.{js,jsx,ts,tsx}": [ 19 | "secretlint", 20 | "prettier --write" 21 | ], 22 | "*.{md,mdx}": [ 23 | "secretlint", 24 | "cspell", 25 | "prettier --write" 26 | ], 27 | "libs/client/src/**/*.ts": [ 28 | "npm run docs:typedoc" 29 | ] 30 | }, 31 | "dependencies": { 32 | "@inquirer/prompts": "^3.3.0", 33 | "@inquirer/select": "^1.3.1", 34 | "@msgpack/msgpack": "^3.0.0-beta2", 35 | "@oclif/core": "^2.3.0", 36 | "@oclif/plugin-help": "^5.2.5", 37 | "@remix-run/dev": "^2.11.1", 38 | "@remix-run/node": "^2.11.1", 39 | "axios": "^1.0.0", 40 | "chalk": "^5.3.0", 41 | "change-case": "^4.1.2", 42 | "chokidar": "^3.5.3", 43 | "commander": "^11.1.0", 44 | "core-js": "^3.6.5", 45 | "cors": "^2.8.5", 46 | "cross-fetch": "^3.1.5", 47 | "dotenv": "^16.3.1", 48 | "encoding": "^0.1.13", 49 | "eventsource-parser": "^1.1.2", 50 | "execa": "^8.0.1", 51 | "express": "^4.18.2", 52 | "fast-glob": "^3.2.12", 53 | "http-proxy": "^1.18.1", 54 | "http-proxy-middleware": "^2.0.6", 55 | "js-base64": "^3.7.5", 56 | "next": "^14.2.5", 57 | "open": "^10.0.3", 58 | "ora": "^8.0.1", 59 | "react": "^18.2.0", 60 | "react-dom": "^18.2.0", 61 | "regenerator-runtime": "0.13.7", 62 | "robot3": "^0.4.1", 63 | "ts-morph": "^17.0.1", 64 | "tslib": "^2.3.0" 65 | }, 66 | "devDependencies": { 67 | "@commitlint/cli": "^17.0.0", 68 | "@commitlint/config-conventional": "^17.0.0", 69 | "@excalidraw/excalidraw": "^0.17.0", 70 | "@inrupt/jest-jsdom-polyfills": "^3.0.1", 71 | "@nrwl/express": "16.10.0", 72 | "@nx/cypress": "16.10.0", 73 | "@nx/eslint-plugin": "16.10.0", 74 | "@nx/express": "16.10.0", 75 | "@nx/jest": "16.10.0", 76 | "@nx/js": "16.10.0", 77 | "@nx/linter": "16.10.0", 78 | "@nx/next": "^16.10.0", 79 | "@nx/node": "16.10.0", 80 | "@nx/react": "16.10.0", 81 | "@nx/web": "16.10.0", 82 | "@nx/webpack": "16.10.0", 83 | "@nx/workspace": "16.10.0", 84 | "@secretlint/secretlint-rule-pattern": "^7.0.7", 85 | "@secretlint/secretlint-rule-preset-recommend": "^7.0.7", 86 | "@sveltejs/kit": "^2.5.0", 87 | "@swc-node/core": "^1.10.6", 88 | "@swc-node/register": "^1.6.8", 89 | "@testing-library/react": "14.0.0", 90 | "@theunderscorer/nx-semantic-release": "^2.2.1", 91 | "@types/cors": "^2.8.14", 92 | "@types/express": "4.17.13", 93 | "@types/jest": "29.4.4", 94 | "@types/node": "18.14.2", 95 | "@types/react": "18.2.24", 96 | "@types/react-dom": "18.2.9", 97 | "@typescript-eslint/eslint-plugin": "5.62.0", 98 | "@typescript-eslint/parser": "5.62.0", 99 | "autoprefixer": "10.4.13", 100 | "babel-jest": "29.4.3", 101 | "cspell": "^8.0.0", 102 | "cypress": "^11.0.0", 103 | "eslint": "8.46.0", 104 | "eslint-config-next": "^14.0.3", 105 | "eslint-config-prettier": "^8.1.0", 106 | "eslint-plugin-cypress": "2.15.1", 107 | "eslint-plugin-import": "2.27.5", 108 | "eslint-plugin-jsx-a11y": "6.7.1", 109 | "eslint-plugin-react": "7.32.2", 110 | "eslint-plugin-react-hooks": "^4.6.0", 111 | "fs-extra": "^11.1.0", 112 | "hono": "^4.6.3", 113 | "husky": "^8.0.0", 114 | "jest": "29.4.3", 115 | "jest-environment-jsdom": "29.4.3", 116 | "jest-environment-node": "^29.4.1", 117 | "lint-staged": "^15.0.2", 118 | "nx": "16.10.0", 119 | "nx-cloud": "16.4.0", 120 | "organize-imports-cli": "^0.10.0", 121 | "postcss": "8.4.21", 122 | "prettier": "^3.3.3", 123 | "prettier-plugin-organize-imports": "^3.2.4", 124 | "prettier-plugin-tailwindcss": "^0.6.5", 125 | "secretlint": "^7.0.7", 126 | "tailwindcss": "3.2.7", 127 | "ts-jest": "29.1.1", 128 | "ts-node": "^10.9.1", 129 | "ts-protoc-gen": "^0.15.0", 130 | "tsconfig-paths": "^4.2.0", 131 | "typedoc": "^0.26.7", 132 | "typedoc-github-theme": "^0.1.2", 133 | "typedoc-plugin-extras": "^3.1.0", 134 | "typedoc-plugin-mdn-links": "^3.2.12", 135 | "typescript": "^5.5.4" 136 | }, 137 | "prettier": { 138 | "plugins": [ 139 | "prettier-plugin-organize-imports", 140 | "prettier-plugin-tailwindcss" 141 | ] 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@fal-ai/client": ["libs/client/src/index.ts"], 19 | "@fal-ai/client/endpoints": ["libs/client/src/types/endpoints.ts"], 20 | "@fal-ai/create-app": ["libs/create-app/src/index.ts"], 21 | "@fal-ai/server-proxy": ["libs/proxy/src/index.ts"], 22 | "@fal-ai/server-proxy/express": ["libs/proxy/src/express.ts"], 23 | "@fal-ai/server-proxy/nextjs": ["libs/proxy/src/nextjs.ts"] 24 | } 25 | }, 26 | "exclude": ["node_modules/**", "tmp/**", "dist/**"] 27 | } 28 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "out": "docs/reference", 4 | "entryPoints": ["./libs/client/src/index.ts"], 5 | "exclude": [ 6 | "./src/__tests__/**", 7 | "*.spec.ts", 8 | "./libs/client/src/types/endpoints.ts" 9 | ], 10 | "excludeExternals": true, 11 | "excludeInternal": false, 12 | "includeVersion": false, 13 | "githubPages": true, 14 | "plugin": [ 15 | "typedoc-plugin-mdn-links", 16 | "typedoc-plugin-extras", 17 | "typedoc-github-theme" 18 | ], 19 | "readme": "none", 20 | "hideGenerator": true 21 | } 22 | --------------------------------------------------------------------------------