DEV Community

loading...
Cover image for Introducing Envelop - The GraphQL Plugin System
The Guild

Introducing Envelop - The GraphQL Plugin System

TheGuildBot
Originally published at the-guild.dev Updated on ・11 min read

This article was published on 2021-07-22 by Dotan Simha @ The Guild Blog

Today we are super excited to share with you a new open-source library we’ve been working on for the past few months!

Envelop

TL;DR

  • Envelop aims to be The GraphQL Plugin system (envelop.dev)
  • Envelop is not a GraphQL server, it's just a wrapper on top of the GraphQL engine.
  • Make “hard” GraphQL capabilities easy by installing powerful plugins (Caching, Tracing with Prometheus/DataDog/NewRelic/Sentry/OpenTelemetry/ApolloTracing, Loggers, GraphQL-Jit, Persisted Operations, Security with rate-limit/depth-limit/Auth0 and many others from the Plugins Hub)
  • Solve once and share across the ecosystem - Each plugin works with any HTTP server or deployment (Express/Fastify/Netlify/Vercel/AWS Lambda/Azure Functions/Cloudflare Workers/Google Cloud Functions) and any schema builder (SDL, Apollo Federation, Nexus, TypeGraphQL and others)
  • Framework for Frameworks - Envelop will become the new basis for GraphQL Frameworks. It's already availble if you are using RedwoodJS, and we have PRs open for Loopback, NestJS, Parse and others.
  • "Babel for GraphQL" - Envelop also aims to be the "enrichment layer" for GraphQL. You can use any new GraphQL Features today (@defer/@stream, @live queries, OneOf and any open RFC already today, even if graphql-js has not yet implemented or released it)
  • envelop is also available on ProductHunt!

Overview

Envelop

Envelop is a lightweight library that allows developers to create plugins that enriches the GraphQL execution layer with new features. It’s the plugin system for your GraphQL layer.

Envelop’s core is based on hooks and plugins - we believe that developers should share and open-source small pieces of implementation and logics that can help others, while still keeping their codebase customized to their needs with full control and power.

Envelop is schema-agnostic and HTTP-server agnostic, meaning that it can be integrated with any kind of setup. We do not aim to provide a complete, vendor-locking suite, since we believe that the developer should be able to adjust any part of their application, at any time, without major implications.

As with any open-source created and maintained by The Guild - we created Envelop based on real-life use-cases, coming from our clients (startups, enterprises and our own products) and from the GraphQL community. We strive to keep our open-source modern, well maintained and always up-to-date, and support the community around it.

Background

While working with many clients on GraphQL projects, we noticed a major gap in collaboration across projects, and a gap in knowledge sharing.

Things were over complicated, and GraphQL servers just kept reinventing the wheel.

We believe these gaps were created because many GraphQL frameworks are focused on creating a “whole” experience, sometimes to promote their stack/product, rather than introducing real flexibility for developers.

Also, as GraphQL keeps evolving with new capabilities and solutions, it seems like the GraphQL frameworks are making it hard or even impossible to use these new features like @defer / @stream, @live queries, @oneOf and other new GraphQL features.

We tried to locate the core of that issue, and from our point-of-view, it seemed like GraphQL was missing a robust, simple and flexible plugin system. That’s why we created Envelop.

While most existing implementations of GraphQL servers/frameworks introduce feature-rich environments, Envelop aims to introduce only hooks on top of the original GraphQL functions, without modifying the signature, and allow you to choose the features that you need, by adding Envelop plugins.

Most existing GraphQL servers are implemented in a way that implements schema building and HTTP server integration, meaning the features that are only relevant to the GraphQL layer “leaks” and creates a very opinionated product.

We believe that the Network Transport <> GraphQL Engine <> GraphQL Schema coupling should be separated, and each part should take care of it’s role, without mixing these features. Each layer has it's own responsibility.

That’s why we decided to create an agnostic library where you can choose your transport (HTTP / WebSocket / anything else), choose your schema (any schema builder works with Envelop), and Envelop will take care of the extra features.

We also felt that for too long things haven't been moving on the server area when it comes to GraphQL - most servers are in maintenance/support mode and don't bring anything new.

Many extra features of GraphQL are straightforward, but not available for developers since it’s not open-source (or, bundled into specific frameworks/servers), or not transparent enough (like, tracing, metrics, auditing, fine-grained permissions and more). We aim to change that.

The envelop approach

One of the goals of Envelop is to allow developers to modify/enrich their GraphQL execution layer.

In most implementations, running a GraphQL operation consists of the following actions:

  • parse - takes raw GraphQL operation string and converts it into an executable DocumentNode.
  • validate - AST based validations, that checks the DocumentNode against the GraphQL schema.
  • contextBuilding - builds a GraphQL execution context, based on the incoming request, and prepares for the execution.
  • variables - parses the input variables and builds the variables object.
  • execute - takes a GraphQL schema, operation DocumentNode, variables and context and runs your resolvers.

There are more phases, and more workflows - It’s dropped only for brevity ;)

Envelop allows developers to create plugins that hook into any phase, and change the behaviour of it, based on the feature it implements. The output of envelop are the GraphQL functions, with the injected behaviour based on the plugins you use.

Envelop

Very initial draft of what Envelop is.

By creating these plugins, you can create custom behaviour in a very easy way.

Let’s try to break a few plugins and understand how it works:

  • useLogger - hooks into the “before” of all phases, and just does console.log.
  • useTiming - hooks into “before” and “after” of all phases, measures times, and then prints it.
  • useParserCache - hooks into before and after the parse phase and implements caching based on the operation string.
  • useGraphQLJit - hooks into execute phase and replaces the execute function with GraphQL-Jit’s executor.
  • usePersistedOperations - hooks into parse and replaces the parse function with a function that maps a hash into a DocumentNode.
  • useGenericAuth - hooks into context building and resolves the current user from the GraphQL request, then hooks into the execute phase to verify the user authentication.
  • useOpenTelemetry - hooks into all phases, execution and resolvers, and creates Spans for OpenTelemetry tracing.

Makes sense, right? Because if you have control of all the execution pipeline, you can easily create very sophisticated plugins that implement things that were missing before with GraphQL, without changing/forking GraphQL.

Envelop

Getting Started

To get started with Envelop, make sure you understand the other requirements that you need:

  • You need a GraphQL schema - it doesn’t matter how you created it (either with GraphQL core library, makeExecutableSchema, or any code-first / schema-first frameworks)
  • You need a HTTP server - like express, Fastify, Koa AWS Lambda or others
  • You need a request normalization and GraphQL request pipeline - we recommend graphql-helix for that.

You can also find more in-depth article and technical documentation here

To get started quickly, start by installing only @envelop/core package in your project:

yarn add @envelop/core

Now, take a look at the following code snippet - it creates a /graphql endpoint, normalizes the incoming request with graphql-helix, creates the GraphQL functions with Envelop and runs the operation:

import { envelop, useSchema, useLogger } from '@envelop/core';
import fastify from 'fastify';
import { processRequest, getGraphQLParameters } from 'graphql-helix';

// This creates the `getEnveloped` function for us. Behind the scense the wrapped functions are created once, here.
const getEnveloped = envelop({
  plugins: [useSchema(schema), useLogger()],
});
const app = fastify();

app.route({
  method: ['POST'],
  url: '/graphql',
  async handler(req, res) {
    // Here we can pass the request and make available as part of the "context".
    // The return value is the a GraphQL-proxy that exposes all the functions.
    const { parse, validate, contextFactory, execute, schema } = getEnveloped({
      req,
    });
    const request = {
      body: req.body,
      headers: req.headers,
      method: req.method,
      query: req.query,
    };
    const { operationName, query, variables } = getGraphQLParameters(request);

    // Here, we pass our custom functions to Helix, and it will take care of the rest.
    const result = await processRequest({
      operationName,
      query,
      variables,
      request,
      schema,
      parse,
      validate,
      execute,
      contextFactory,
    });

    if (result.type === 'RESPONSE') {
      res.status(result.status);
      res.send(result.payload);
    } else {
      // You can find a complete example with Subscriptions and stream/defer here:
      // https://github.com/contrawork/graphql-helix/blob/master/examples/fastify/server.ts
      res.send({ errors: [{ message: 'Not Supported in this demo' }] });
    }
  },
});

app.listen(3000, () => {
  console.log(`GraphQL server is running...`);
});
Enter fullscreen mode Exit fullscreen mode

With that example, we only used useLogger, so while executing GraphQL operations, you should see that everything you do should be printed to the log.

Use Plugins

Plugins

But logging is not everything possible with Envelop. By adding more plugins, you can add more features to your GraphQL execution, based on your app needs.

For example, here’s a cool snippet for boosting things in your execution layer:

const getEnveloped = envelop({
  plugins: [
    useSchema(schema),
    useParserCache(),
    useValidationCache(),
    useGraphQLJit(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

By using useParserCache we make sure to parse every unique operation only once.
By using useValidationCache we make sure to validate every unique operation only once.
By using useGraphQLJit we replace the default execute function with a just-in-time implementation.

While working with our clients, we saw that many pieces of code can be moved into an Envelop plugin, and shared with the community. That created tons of plugins that you can now use quickly, without implementing it on your own for that specific project!

We also created Envelop Plugins Hub : a place where you can find all the plugins that are available for Envelop, with their documentation, versions, and some stats. Plugin Hub is open and available for the community to add their own.

Write your own plugins

Plugins

Writing plugins for Envelop is super simple. We allow you to write code that connects to the phases that you need, and we’ll make sure to run your functions at the right time.

Plugins can either live as internal plugins that are relevent only to your project, or you can share it with the community as NPM package.

To get started with a custom plugin, choose what phases you need, and create functions that handle what you need. Envelop will provide a low-level, flexible api in each phase, so you can communicate with the core pipeline.

import { Plugin } from '@envelop/types';

const myPlugin: Plugin = {
  onParse({ params }) {
    console.log('Parse started!', { args });

    return (result) => {
      console.log('Parse done!', { result });
    };
  },
  onExecute({ args }) {
    console.log('Execution started!', { args });

    return {
      onExecuteDone: ({ result }) => {
        console.log('Execution done!', { result });
      },
    };
  },
};

const getEnveloped = envelop({
  plugins: [
    /// ... other plugins ...,
    myPlugin,
  ],
});
Enter fullscreen mode Exit fullscreen mode

You can find here the complete plugins documentation

Sharing envelops

Share

In many cases, developers are looking for a way to reuse their server setup, as a boilerplate/template. Envelop allows you to create Envelops instances and later share it with others.

import { envelop, useEnvelop, useSchema } from '@envelop/core';

// Somewhere where you wish to create the basics of what you wish to share
// This defined the base plugins you wish to use as base.
const myBaseEnvelop = envelop({
  plugins: [useOrgAuth(), useOrgTracing(), useOrgLogsCollector()],
});

// Later, when you create your own Envelop, you can extend that and add custom plugins.
// You can also specify the schema only at this point
const myEnvelop = envelop({
  plugins: [
    useEnvelop(myBaseEnvelop),
    useSchema(myServerSchema),
    useMyCustomPlugin(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

So if you are working in a microservices environment, or in an enterprise that has many servers - you can now share the entire base GraphQL setup in a single variable, and extend it based on your needs.

You can read more about sharing/composing envelops here

"Babel for GraphQL" - New Features for the GraphQL Engine

Since we allow developers to take part in any phase of the execution, it means that you can easily add new features for the GraphQL engine, and not just features that come on top of GraphQL.

For example, one of the Envelop plugins (useExtendedValidation) allows developers now to write and run GraphQL validations, with access to the operation variables. That means you can write simple validations now without making it part of your schema.

One of the things that is also possible now is @oneOf - a spec suggestion that is still in discussion for adding input unions, but already available for you if you use Envelop, because extended validations can access variables and can do additional things that was difficult to do before.

Here are some additional examples for cool new plugins:

Adoption and Migration Path / Framework for Frameworks

If you are already using GraphQL, you are probably using a server that comes with all the features built-in. This is great in some cases, but if you wish to have that extra flexibility, you can migrate to Envelop. You can even use Envelop with other server frameworks without migrating the entire pipeline (see examples section below).

GraphQL is also widely adopted in the JAMStack world - and libraries that offer GraphQL out of the box migrate to Envelop to simplify parts of the code, and to allow their users to extend the GraphQL layer in a simple way.

Redwood is a great example. We start with a small suggestion PR, and the Redwood team was open for new ideas - so now you can use envelop if you are a Redwood user !

Here is a thread about why Redwood now gives you the option to replace Apollo Server with GraphQL-Helix + Envelop.

During that process, we also start to work with other frameworks and support them with that: Loopback, NestJS, Parse, Apollo Server and others.

We are also helping with that, so if you are migrating to Envelop and not sure what it includes/means for your project - feel free to reach out to us (through GitHub, email or the chat box in our website) and we would love to help you with that.

Examples

Since we understand that Envelop doesn’t come as a whole server, we create tons of examples you can use for reference. We added examples for using several HTTP servers (express/fastify), running different Functions/Lambda cloud providers, different schema providers (Type-GraphQL, Nexus) subscriptions transports (SSE / GraphQL-WS), new GraphQL features like @stream / @defer and more.

You can find all examples and demos here

What’s next?

We are constantly working on improving the low-level API of Envelop, so if something is missing, you can always reach out and report an issue. We are also adding more plugins based on our use-cases.

Like with any other open-source maintained by The Guild, we always welcome you to share your thoughts, ideas, feedback, questions and issues. We also encourage developers to take an active part in the development of the products/libraries they are using - so if you think something you wrote can benefit others - we can help with making it a reality!

Discussion (0)