JavaScript runs everywhere, so should your servers - here is how

Arda Tanrikulu
Looking for experts? We offer consulting and trainings.
Explore our services and get in touch.

TLDR

  • We made GraphQL Yoga 2.0 platform-agnostic, so it is able to run anywhere JavaScript can run (Cloudflare Workers, Deno, Next.js. AWS Lambdas etc.) thanks to the new Fetch API standard
  • We've created Ponyfills so it will work the same on older versions of Node that don't have Fetch API elements globally available
  • We've made a new general library out of it so that any other framework or app could achieve the same
  • Let's help other frameworks in the ecosystem to migrate to this new library and standard

At the beginning of the year, we launched GraphQL Yoga 2.0 - a server framework for GraphQL APIs.

While planning version 2.0 of Yoga, we were thinking about all the things that changed in the ecosystem and what developers using JavaScript expect from their server frameworks now and in the future.

One of the most powerful trend in the JS ecosystem was the proliferation of new environments and platforms that can run JS (Lambdas, Cloudflare Workers, Deno, Bun etc.). So we set up to build a single GraphQL server framework that could run on any of these platforms.

What we’ve found in the process was fascinating, and we believe would change how any JS HTTP frameworks are being built, without any relation to GraphQL

While Node.js is the most popular environment, multiple platforms can run JavaScript, usually, having their own way of creating servers, and using different APIs.

On the other hand, the JavaScript client-side has mostly migrated over the years to a common set of standards (fetch), independently of the underlying platform.

This is where we realized that WHATWG Fetch API that uses Request, Response, and ReadableStream could also be used on the server side to provide a unified API to run JavaScript HTTP servers everywhere.

Why Fetch API Standard?

When you send a request from the client, fetch(...requestArgs) uses Request object under the hood that contains all the details(headers, method, etc) and the data stream you need for that communication. Then you take the Response object that contains the connection stream, and process it as you want with .json(), .arrayBuffer(), .formData() or just access the stream itself by .body() as ReadableStream

On the server side, you can take Request object and process it with the same methods without dealing with the internals of your platform.

Streaming responses like SSE uses ReadableStream, and for Multipart requests (e.g. file uploads) uses FormData, which is exactly the same forwarded from the browser or any other client using Fetch API.

You can see how easy to handle file uploads;

const formData = await request.formData()
const myFile = await formData.get('myFile')
const fileContents = await myFile.text()

See more in our code; https://github.com/dotansimha/graphql-yoga/blob/master/packages/common/src/plugins/requestParser/POSTMultipart.ts#L18

Streaming responses like Server Sent Events example:

let interval
new Response(
  new ReadableStream({
    start() {
      interval = setInterval(() => {
        this.enqueue(`data: ${Date.now()}\n\n`)
      }, 1000)
    },
    cancel() {
      clearInterval(interval)
    }
  }),
  {
    headers: {
      'Content-Type': 'text/event-stream'
    }
  }
)

See more in our code; https://github.com/dotansimha/graphql-yoga/blob/master/packages/common/src/plugins/resultProcessor/push.ts#L42

What about Node.js?

Even though many new platforms support the Fetch API Standard, which means we can have a single solution for all, currently, in the older LTS versions of Node.js, we don’t have an implementation of the Fetch API built in.

Furthermore, Node.js doesn't use Web standard streams and the Fetch API in its http and https modules.

That’s why we created the @whatwg-node/fetch package (previously known as cross-undici-fetch ) that fills in the gaps of different fetch implementations in all the LTS Node.js versions. Under the hood, @whatwg-node/fetch utilizes undici if available or otherwise falls back to using node-fetch, which you are probably already familiar with.

In case @whatwg-node/fetch is imported in an environment that already has Fetch API built in like Cloudflare Workers, no ponyfills are added to your built application bundle.

Ponyfill vs Polyfill

Polyfill patches the native parts of an environment while ponyfill just exports the “patched” stuff without touching the environment’s internals. We prefer pony filling because it prevents us from breaking other libraries and environmental functionalities.

Is it possible to have a library that creates a cross-platform server?

When rebuilding GraphQL Yoga from scratch, cross-platform support was one of the most important features we wanted to implement. We wanted to create a GraphQL server library that can be integrated with different Node.js server frameworks and other JS environments like CF Workers and Deno with a few additional lines of code.

After a few iterations, it was clear to us that this is certainly possible and finally shipped this as part of GraphQL Yoga v2.

GraphQL Yoga instance itself can be used directly as a request listener that you pass to Express’s app.use, Node’s native http.createServer, Next.js functions and other non-Node.js environments; we just pass GraphQL Yoga as an event listener for CF Workers’ self.addEventListener('fetch', yoga).

As we already mentioned in the “Why Fetch API?” part, the server library itself doesn't need to care about the platform’s connection-specific details like Node’s IncomingMessage and ServerResponse or Next.js’s NextApi.Request objects. You are now able to focus on your server implementation details by consuming a “universal standard” Request and returning “another standard” Response

As we realized how well this works out for our users, we decided that we need to bring this to the next level and extract that logic into a standalone library called @whatwg-node/server .

You simply provide your request handler that has a single Request parameter and expects you to return a Response instance. The generated request handler instance can be integrated with regular Node HTTP servers, Fastify, Koa, Deno, CF Workers, Next.js, etc with a few lines of code.

import { createServerAdapter } from '@whatwg-node/server'
import { Request, Response } from '@whatwg-node/fetch'
 
const myServer = createServerAdapter({
  handle(request: Request) {
    return new Response('Hello world', {
      status: 200
    })
  }
})
 
// Node.js
import { createServer } from 'http'
 
const nodeServer = createServer(myServer)
nodeServer.listen(4000)
 
// CF Workers
self.addEventListener('fetch', myServer)
 
// Next.js
export default myServer
 
// Deno
serve(myServer, { addr: ':4000' })

How does it look in real-life usage today?

You can check the GraphQL Yoga repository to see how we use this library;

https://github.com/dotansimha/graphql-yoga/blob/no-more-node/packages/graphql-yoga/src/server.ts#L628

And the simplicity of the integrations in our examples;

https://github.com/dotansimha/graphql-yoga/tree/no-more-node/examples

Finally how small the code is when we want to process the request and the response objects;

https://github.com/dotansimha/graphql-yoga/tree/no-more-node/packages/graphql-yoga/src/plugins

There is literally nothing platform-specific in GraphQL Yoga, and this allows us to focus on creating a good GraphQL Server implementation for the entire GraphQL JS ecosystem.

Node.js Server Frameworks, Routers & Middlewares

Node.js solved issues years before the web standards could. However, we think that many server-side ideas inspired by Node.js can now easily be managed with JavaScript with Fetch, Web Streams and other web standards in the JavaScript ecosystem.

There are many mature libraries such as Fastify, Koa, Express, and Hapi that are implemented only for Node.js without using the Fetch API Standard. The experience with those libraries in the current era of Node.js taught us a lot about how the server can be designed but maybe it is time to reduce the environment-specific APIs in the JS ecosystem.

The major reason for using a server framework is usually “Routing” then “Middlewares” so the question is “Why cannot we just have that with Fetch API?”

You can basically achieve routing like below, however it looks a bit unsafe.

createServerAdapter({
  handle(request: Request) {
    if (request.url.endsWith('/hello')) {
      return new Response('{ "message": "hello" }', {
        status: 200,
        headers: {
          'Content-Type': 'application/json'
        }
      })
    }
    if (request.url.endsWith('/secret')) {
      return new Response('No way!', {
        status: 401
      })
    }
    return new Response('Nothing here!', {
      status: 404
    })
  }
})

There is another library called itty-router which can be used for Routing with Fetch API. You can see how simple it is to achieve “Routing” in a platform-independent way.

import { Router } from 'itty-router'
import { createServerAdapter } from '@whatwg-node/server'
 
// now let's create a router (note the lack of "new")
const router = Router()
 
// GET collection index
router.get('/todos', () => new Response('Todos Index!'))
 
// GET item
router.get('/todos/:id', ({ params }) => new Response(`Todo #${params.id}`))
 
// POST to the collection (we'll use async here)
router.post('/todos', async request => {
  const content = await request.json()
 
  return new Response('Creating Todo: ' + JSON.stringify(content))
})
 
// 404 for everything else
router.all('*', () => new Response('Not Found.', { status: 404 }))
 
// attach the router "handle" to our server adapter
const myServer = createServerAdapter(router)
 
// Then use it in any environment
import { createServer } from 'http'
 
const httpServer = createServer(myServer)
httpServer.listen(4000)

What NodeJS frameworks are you using today?

Maybe it is worthwhile to open a new issue on their repo and see if we could all help them to become platform-agnostic, while still supporting older versions of Node, thanks to this new library.

Please try it out and give us feedback on the repo!

Last updated on September 17, 2022

Join our newsletter

Want to hear from us when there's something new? Sign up and stay up to date!

By subscribing, you agree with Revue’s Terms of Service and Privacy Policy.

Recent issues of our newsletter