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

Http api #83

Open
florianbepunkt opened this issue Nov 16, 2024 · 4 comments
Open

Http api #83

florianbepunkt opened this issue Nov 16, 2024 · 4 comments
Labels
enhancement New feature or request

Comments

@florianbepunkt
Copy link

I would love to see a way to use the http api from @effect/platform with a lambda fn fronted by api gateway. Currently we use middy and their http router in our code base for some microservices.

@florianbepunkt
Copy link
Author

To be more precise, I believe two functionalities could be beneficial, although maybe out of scope for this package (especially the latter).

(1) A function similar to HttpApiBuilder.toWebHandler and https://github.com/floydspace/effect-aws/tree/main/packages/lambda that will take an Http api and returns a handler function that takes an APIGatewayProxyEvent as input and produces an APIGatewayProxyResult as output

(2) It should be possible to take the http api definition and derive a CDK api gateway from it that can be deployed, similar to OpenAPI cdk constructs.

@floydspace
Copy link
Owner

floydspace commented Nov 17, 2024

hi @florianbepunkt
I think anything related to aws coulb be part of this monorepo, I'm not against it.
I know @AMar4enko had implemented something like what you describe in the (2), I believe this one.
I personally did not use httpapi yet, still in my list, I will explore it, but if you are welcome to contribute if you already have implementation in mind, if not at least you can draft some interface of usage, which would be a helpful starting point.

@floydspace floydspace added the enhancement New feature or request label Nov 17, 2024
@florianbepunkt
Copy link
Author

@floydspace Thanks for the link. This is an interesting proof of concept.

I believe the CDK construct is the more difficult part. We have several APIs, where we have an OpenAPI spec that uses entities defined by Effect Schema. This OpenAPI spec file is used to create a rest api CDK construct, similar to what the poc does. The problem, which would also apply to the poc you have linked: Effect produces JSON schema v7 while AWS api gateway can only handle draft4. With slightly more complex schemas you will unfortunately run into problems :/ Therefore we had to use a custom JSON schema generator to create draft4 JSON schemas. I opened an issue here: AMar4enko/effect-http-api-gateway#1 Maybe AMar4enko has came up with a way to solve this.

I'm not sure whether using OpenAPI is the right approach. If you define the API in effect, you are already doing the validation there. So while there is probably no harm in double validating requests, it is not necessary. Plus you get the limitations mentioned above.

With regards to (1):

I believe something around the following lines should work (untested):

import { DateTime, Effect, Layer, Schema as S } from "effect";
import {
  HttpApi,
  HttpApiBuilder,
  HttpApiEndpoint,
  HttpApiGroup,
  HttpApp,
  HttpServer,
  type HttpRouter,
} from "@effect/platform";
import type { Router } from "@effect/platform/HttpApiBuilder";
import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";

const eventToNativeRequest = (event: APIGatewayProxyEvent): Request => {
  const { httpMethod, headers, body, path, queryStringParameters } = event;

  // Construct the URL
  const protocol = headers["X-Forwarded-Proto"] || "https";
  const host = headers["Host"] || "localhost";
  const queryString = new URLSearchParams(
    (queryStringParameters as Record<string, string>) || {},
  ).toString();
  const url = `${protocol}://${host}${path}${queryString ? `?${queryString}` : ""}`;

  // Map headers to Headers object
  const requestHeaders = new Headers();
  for (const [key, value] of Object.entries(headers || {})) {
    if (value) {
      requestHeaders.append(key, value);
    }
  }

  // Return the Request object
  return new Request(url, {
    method: httpMethod,
    headers: requestHeaders,
    body: body || undefined,
  });
};

const fromNativeResponse = async (response: Response): Promise<APIGatewayProxyResult> => {
  const headers: { [header: string]: string } = {};

  response.headers.forEach((value, key) => {
    headers[key] = value;
  });

  const body = response.bodyUsed ? await response.text() : null;

  return {
    statusCode: response.status,
    headers,
    body: body || "",
    isBase64Encoded: false, // Assume the body is not Base64 encoded by default
  };
};

const makeApiLambda = <LA, LE>(
  layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
  options?: {
    readonly middleware?: (
      httpApp: HttpApp.Default,
    ) => HttpApp.Default<never, HttpApi.Api | Router | HttpRouter.HttpRouter.DefaultServices>;
    readonly memoMap?: Layer.MemoMap;
  },
) => {
  const { handler } = HttpApiBuilder.toWebHandler(layer, options);
  return async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
    const request = eventToNativeRequest(event);
    const response = await handler(request);
    return fromNativeResponse(response);
  };
};

// Usage example

class User extends S.Class<User>("User")({
  id: S.Number,
  name: S.String,
  createdAt: S.DateTimeUtc,
}) {}

class UsersApi extends HttpApiGroup.make("users").add(
  HttpApiEndpoint.get("findById", "/users/:id")
    .addSuccess(User)
    .setPath(
      S.Struct({
        id: S.NumberFromString,
      }),
    ),
) {}

class MyApi extends HttpApi.empty.add(UsersApi) {}

const UsersApiLive: Layer.Layer<HttpApiGroup.ApiGroup<"users">> = HttpApiBuilder.group(
  MyApi,
  "users",
  (handlers) =>
    handlers
      // the parameters & payload are passed to the handler function.
      .handle("findById", ({ path: { id } }) =>
        Effect.succeed(
          new User({
            id,
            name: "John Doe",
            createdAt: DateTime.unsafeNow(),
          }),
        ),
      ),
);

const MyApiLive: Layer.Layer<HttpApi.Api> = HttpApiBuilder.api(MyApi).pipe(Layer.provide(UsersApiLive));

const handler = makeApiLambda(Layer.mergeAll(MyApiLive, HttpServer.layerContext));

@florianbepunkt
Copy link
Author

@floydspace I sketched a rough prototype: https://github.com/florianbepunkt/effect-aws-api

If you find the time, maybe you can have a look. I'm rather unsure how to approach / generalize this in a way that it helps others. The CDK construct make a lot of assumptions about a use case – some might make sense (lambdalith with http api), other might not.

Also there are some open questions I noted in the readme and the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants