Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new aws lambda server proxy #122

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
dist
tmp
/out-tsc
cdk.out

# dependencies
node_modules
Expand Down
3 changes: 3 additions & 0 deletions apps/demo-lambda-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Rename this file to .env.local and add your fal credentials
# Visit https://fal.ai to get started
FAL_KEY="FAL_KEY_ID:FAL_KEY_SECRET"
18 changes: 18 additions & 0 deletions apps/demo-lambda-app/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
74 changes: 74 additions & 0 deletions apps/demo-lambda-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Fal AI Lambda Proxy

This is a serverless proxy for Fal AI services deployed on AWS Lambda using CDK.

## Prerequisites

- Node.js 20.x or later
- AWS CLI configured with appropriate credentials
- AWS CDK CLI installed (`npm install -g aws-cdk`)

## Setup

1. Clone the repository and navigate to the app directory:

```bash
cd apps/demo-lambda-app
```

2. Install dependencies:

```bash
npm install
```

3. Create a `.env.local` file based on the example:

```bash
cp .env.example .env.local
```

4. Add your Fal AI API key to `.env.local`:
```
FAL_KEY=your-fal-ai-key-here
```

## Deployment

1. Bootstrap CDK (if you haven't already):

```bash
cdk bootstrap
```

2. Deploy the stack:
```bash
cdk deploy
```

After deployment, CDK will output the `FalProxyUrl`. Save this URL - you'll need it to configure your client.

## Stack Components

- **Lambda Function**: Runs on Node.js 20.x with ARM64 architecture
- **API Gateway**: HTTP API with CORS enabled
- **Environment Variables**: Securely stores your FAL_KEY

## Usage with fal-js client

Once you have the proxy URL from the CDK output, you can use it with the fal-js client in your frontend application:

```typescript
import * as fal from '@fal-ai/serverless-client'

// Initialize fal client with your proxy URL
fal.config({
proxyUrl: 'YOUR_PROXY_URL_FROM_CDK_OUTPUT' // e.g., https://xxxxx.execute-api.region.amazonaws.com
});

## Security Notes

- Ensure your `.env.local` file is not committed to version control
- The API Gateway is configured to accept requests from any origin (`*`)
- Consider restricting CORS settings in production
```
3 changes: 3 additions & 0 deletions apps/demo-lambda-app/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "npx tsx cdk/demo-lambda-app-stack.ts"
}
55 changes: 55 additions & 0 deletions apps/demo-lambda-app/cdk/demo-lambda-app-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { App, CfnOutput, Duration, Stack, type StackProps } from "aws-cdk-lib";
import {
CorsHttpMethod,
HttpApi,
HttpMethod,
} from "aws-cdk-lib/aws-apigatewayv2";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { Architecture, Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import type { Construct } from "constructs";
import { configDotenv } from "dotenv";
import { join } from "node:path";

configDotenv({ path: ".env.local" });

export class DemoLambdaAppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const handler = new NodejsFunction(this, "FalProxyHandler", {
entry: join(__dirname, "../src/index.ts"),
functionName: "FalProxyHandler",
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
timeout: Duration.seconds(30),
environment: {
FAL_KEY: process.env.FAL_KEY!,
},
bundling: {
sourceMap: true,
},
});

const httpApi = new HttpApi(this, "FalProxyApi", {
apiName: "Fal Proxy API",
corsPreflight: {
allowHeaders: ["*"],
allowOrigins: ["*"],
allowMethods: [CorsHttpMethod.ANY],
},
});
httpApi.addRoutes({
path: "/{proxy+}",
methods: [HttpMethod.POST, HttpMethod.GET],
integration: new HttpLambdaIntegration("DefaultIntegration", handler),
});

new CfnOutput(this, "FalProxyUrl", {
value: httpApi.url || "Something went wrong with the deployment",
});
}
}

const app = new App();
new DemoLambdaAppStack(app, "DemoLambdaAppStack", {});
11 changes: 11 additions & 0 deletions apps/demo-lambda-app/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: "demo-lambda-app",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/apps/demo-lambda-app",
};
64 changes: 64 additions & 0 deletions apps/demo-lambda-app/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "demo-lambda-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/demo-lambda-app/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/demo-lambda-app",
"main": "apps/demo-lambda-app/src/index.ts",
"tsConfig": "apps/demo-lambda-app/tsconfig.app.json",
"assets": ["apps/demo-lambda-app/src/assets"],
"isolatedConfig": true,
"webpackConfig": "apps/demo-lambda-app/webpack.config.js"
},
"configurations": {
"development": {},
"production": {}
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"options": {
"buildTarget": "demo-lambda-app:build"
},
"configurations": {
"development": {
"buildTarget": "demo-lambda-app:build:development"
},
"production": {
"buildTarget": "demo-lambda-app:build:production"
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/demo-lambda-app/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/demo-lambda-app/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
Empty file.
1 change: 1 addition & 0 deletions apps/demo-lambda-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { handler } from "@fal-ai/server-proxy/lambda";
10 changes: 10 additions & 0 deletions apps/demo-lambda-app/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "ESNext",
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
16 changes: 16 additions & 0 deletions apps/demo-lambda-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}
14 changes: 14 additions & 0 deletions apps/demo-lambda-app/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
8 changes: 8 additions & 0 deletions apps/demo-lambda-app/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { composePlugins, withNx } = require("@nx/webpack");

// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});
4 changes: 2 additions & 2 deletions apps/demo-nextjs-page-router/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function Index() {
setLoading(true);
const start = Date.now();
try {
const result = await fal.subscribe<Output>("fal-ai/lora", {
const result = await fal.subscribe("fal-ai/lora", {
input: {
prompt,
model_name: "stabilityai/stable-diffusion-xl-base-1.0",
Expand All @@ -89,7 +89,7 @@ export function Index() {
}
},
});
setResult(result.data);
setResult(result.data as Output);
} catch (error: any) {
setError(error);
} finally {
Expand Down
10 changes: 10 additions & 0 deletions libs/proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ For Express applications:

3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key.

## AWS Lambda integration

For AWS Lambda applications:

1. Re-export the proxy handler from your Lambda handler file
```ts
export { handler } from "@fal-ai/server-proxy/lambda";
```
2. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key.

## Client configuration

Once you've set up the proxy, you can configure the client to use it:
Expand Down
8 changes: 6 additions & 2 deletions libs/proxy/src/hono.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Context } from "hono";
import { type StatusCode } from "hono/utils/http-status";
import { type ContentfulStatusCode } from "hono/utils/http-status";
import {
handleRequest,
HeaderValue,
Expand Down Expand Up @@ -35,7 +35,11 @@ export function createRouteHandler({
id: "hono",
method: context.req.method,
respondWith: (status, data) => {
return context.json(data, status as StatusCode, responseHeaders);
return context.json(
data,
status as ContentfulStatusCode,
responseHeaders,
);
},
getHeaders: () => responseHeaders,
getHeader: (name) => context.req.header(name),
Expand Down
47 changes: 47 additions & 0 deletions libs/proxy/src/lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
} from "aws-lambda";
import { DEFAULT_PROXY_ROUTE, handleRequest } from "./index";

/**
* The default API Gateway route for the fal.ai client proxy.
*/
export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;

/**
* The API Gateway route handler for the fal.ai client proxy.
* Use it as a handler for AWS Lambda
*
* Note: This proxy doesn't support streaming responses.
*
* @param request the API Gateway request object.
* @returns a promise that resolves when the request is handled.
*/

export async function handler(
request: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> {
const response: APIGatewayProxyResultV2 = { headers: {} };

return handleRequest({
id: "lambda-fal-proxy",
method: request.requestContext.http.method,
getRequestBody: async () => request.body,
getHeaders: () => request.headers,
getHeader: (name: string) => request.headers[name],
sendHeader: (name: string, value: string) => {
response.headers![name] = value;
},
respondWith: (status: number, data: string) => {
response.statusCode = status;
response.body = data;
return response;
},
sendResponse: async (res: Response) => {
response.statusCode = res.status;
response.body = await res.text();
return response;
},
});
}
Loading