sukovanej / effect-http

Declarative HTTP API library for effect-ts
https://sukovanej.github.io/effect-http
MIT License
248 stars 20 forks source link

[IDEA] Integration with Express #548

Closed Almaju closed 4 months ago

Almaju commented 5 months ago

I am currently working on an Express based codebase and I am trying to switch to effect-http in a non-breaking way. I wrote a small adapter to wrap Express and be able to do that, I thought this could be of interest. Not sure this deserves its own package but it could be useful as an example somewhere?

This is what it looks like:

const expressApp = express();

const app = pipe(
  Api.make({ title: 'Users API' }),
  Api.addEndpoint(...),
  RouterBuilder.make,
  RouterBuilder.handle(...),
  RouterBuilder.mapRouter(Middleware.from(expressApp).apply), // <-- where the magic happens
  RouterBuilder.buildPartial,
);

The implementation:

// request.ts
import { HttpServer } from '@effect/platform';
import { NodeHttpServer } from '@effect/platform-node';
import { Data, Effect } from 'effect';
import { IncomingMessage } from 'http';

export class Request extends Data.Class<IncomingMessage> {
  static ask() {
    return Effect.gen(function* (_) {
      return Request.from(yield* _(HttpServer.request.ServerRequest));
    });
  }

  static from(req: HttpServer.request.ServerRequest) {
    return new Request(NodeHttpServer.request.toIncomingMessage(req));
  }
}
// response.ts
import { HttpServer } from '@effect/platform';
import { Data, Effect } from 'effect';
import { EventEmitter } from 'events';
import { Response as ExpressResponse } from 'express';
import httpMocks from 'node-mocks-http';

export class Response extends Data.Class<
  httpMocks.MockResponse<ExpressResponse>
> {
  static make() {
    return new Response(
      httpMocks.createResponse({
        eventEmitter: EventEmitter,
      }),
    );
  }

  resolve = () =>
    Effect.async<void, Error>((resolve) => {
      this.on('end', () => {
        resolve(Effect.void);
        this.on('error', (e) => resolve(Effect.fail(e)));
      });
    });

  into = () =>
    HttpServer.response.text(this._getData(), {
      contentType: 'application/json',
      headers: HttpServer.headers.fromInput(
        this.getHeaders() as HttpServer.headers.Input,
      ),
      status: this._getStatusCode(),
      statusText: this._getStatusMessage(),
    });
}
// middleware.ts
import { HttpServer } from '@effect/platform';
import { Data, Effect, pipe } from 'effect';
import { Express } from 'express';

import { Request } from './request';
import { Response } from './response';

export class Middleware extends Data.Class<{ express: Express }> {
  static from(express: Express) {
    return new Middleware({ express });
  }

  apply = <E, R>(
    router: HttpServer.router.Router<E, R>,
  ): HttpServer.router.Router<E | Error, R> =>
    HttpServer.router.all(
      router,
      '*',
      Effect.gen(this, function* (_) {
        const request = yield* _(Request.ask());
        return yield* _(this.request(request));
      }),
    );

  request = (request: Request) =>
    Effect.gen(this, function* (_) {
      const response = Response.make();
      this.express(request, response);
      yield* _(response.resolve());
      return response.into();
    });
}
sukovanej commented 5 months ago

Hey, nice job! This is definitely worth sharing on the Effect discord and maybe even make this example part of the official @effect/platform readme because I remember few people asked about the express <> /platform interop in the past already. And your impl seems to be independent of this lib so you cover wider audience than just users of effect-http. So go ahead and start a thread in the Effect discord, I'm sure you'll get lot of people excited and you might get a valuable feedback from the community!

sukovanej commented 5 months ago

After the feedback, feel free to open a PR with a readme or example in here, but the official /platform package is maybe even better place. Also, I think it could deserve it's own package potentially. I think the size of this snippet will increase as you'll start tackling different edge cases / problems, again, I recommend to open this topic with the Effect community 🙂.