Skip to content

Latest commit

 

History

History
125 lines (95 loc) · 6.23 KB

README.md

File metadata and controls

125 lines (95 loc) · 6.23 KB

Facade 🎭

When you need to pretend you are a REST API, but you're really not.

Facade is a Rust-based HTTP transformation layer to seamlessly convert REST calls into GraphQL calls for piecemeal API migrations.

Project Status: Experimental (check goals for current progress).

⚡️ Quick Start

  • cargo build: Build the project
  • cargo dev: Run local development server (requires cargo-watch)
  • cargo generate-sdl: Generate a new schema.graphql (in tests/common/)
  • cargo test-nocapture: Run tests but do not hide output

✅ Goals:

  • Seamlessly proxy any HTTP requests through, keeping headers etc intact
  • Support overriding specific URLs for specific HTTP verbs
  • Support easy transformation into a GraphQL request
  • Support easy transformation of GraphQL response data into expected REST output
  • Be easily testable (e.g. make sure it's easy to check a route got directed to the right match)
    • Support for testing e.g. /api/v1/me was mapped, and /api/v1/device was passed through
    • Add header X-FACADE-MATCH that exposes what happened to the path (e.g. MAPPED or PASSTHROUGH) - you should be able to turn this off also

Non Goals:

  • Extending existing GraphQL schema
  • Focusing on anything else than GraphQL (it should be trivial, but GraphQL should be the easy-path)

Motivation

GraphQL is only a recent addition into the API space, with REST having been the predominant way to structure APIs so far. So now you want to get on the sweet journey towards a nicely structured GraphQL API—but wait!—you still need to keep your REST API around because you have old clients that either cannot or will not be updated (e.g. old Mobile App releases).

How do you initiate your API modernization without now needing to maintain two different APIs? This is where facade comes into play.

facade is a service that sits in front of you existing REST API to allow you a piecemeal migration to your new API, by rewriting requests on-the-fly. After mapping an incoming REST call to the new GraphQL query, you can now remove the old code in your REST API.

This project was inspired by a real-world situtation where the backend provided a legacy REST API along with a new GraphQL API. These two APIs were written in different languages and served by different services. Completely removing the REST API was far away from possilbe, because of old clients existing, specifically previous Mobile App releases, that simply couldn't be updated.

On the frontend-side of things, one could easily migrate each new release to the GraphQL API as it became fully fleshed out, but the backend was stuck supporting the REST API in perpetuity. We needed a way to still support the same legacy API calls, but without needing to maintain the legacy code as well—and thus, facade was born.

Implementation Stategy

First off, we'll need to implement:

  • Test servers for REST
  • Test servers for GraphQL

After this, we'll need a couple of cases:

  • Passthrough: Paths that are not overridden
  • Simple case: A direct transformation from REST<->GraphQL (fields map directly)
  • Advanced case: A transformation from REST<->GraphQL which processes the data (fields need transformation)

We can set up a couple of REST endpoints:

  • Passthrough: GET /api/v1/device returns a JSON object { data: { devices: [] } }
  • Simple case: GET /api/v1/uuid returns a JSON object { data : { uuid: "UUIDV4...." } }
  • Simple case: GET /api/v2/uuid returns a JSON object { uuid: "UUIDV4...." }
  • Advanced case: GET /api/v1/me returns a JSON object { data: { username: "Ariel", ... } }

And a GraphQL schema:

type User {
  username: String!
}

Query {
  me: User!
  uuid: String!
}

The REST endpoint should easily support if things are wrapped in something or not (e.g. data).

Ideal Library Design

Let's start from how the user would interact with the library:

use facade::*;

#[tokio::main]
async fn main() -> Result<()> {
    // Set up logging
    fern::Dispatch::new()
        .level(log::LevelFilter::Debug)
        .chain(std::io::stdout())
        .apply();

    // Set up where Facade should proxy and which paths it should overwrite.
    let server = Facade::builder()
      .bind(std::net::SocketAddr::from(([127, 0, 0, 1], 3000)))
      .get("/api/v1/uuid", Facade::direct_graphql_wrapped(GraphQL::Query::uuid, "data"))
      .get("/api/v2/uuid", Facade::direct_graphql(GraphQL::Query::uuid))
      .get("/api/v1/me", me_handler)
      .get("/api/v1/*", "https://httpbin.org")
      .build()
      .unwrap()
      .serve();

    // Start the server.
    server.await
}

Resources

Potentially:

Some blog posts: