JavaScript runs everywhere, so should your servers - here is how
Tags: fetch, whatwg, node, graphql
Published: 2022-08-30 08:05:32

> This article was published on Monday, August 22, 2022 by [Arda Tanrikulu](https://twitter.com/ardatanrikulu) @ [The Guild Blog](https://the-guild.dev/)

## TL;DR

*   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 <ins>Ponyfills</ins> 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**](https://github.com/ardatan/whatwg-node) 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](/blog/announcing-graphql-yoga-v2) - 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`](https://developer.mozilla.org/en-US/docs/Web/API/Request),
[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/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`](https://developer.mozilla.org/en-US/docs/Web/API/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;

```ts
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](https://github.com/dotansimha/graphql-yoga/blob/master/packages/common/src/plugins/requestParser/POSTMultipart.ts#L18)

Streaming responses like
[Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
example:

```ts
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](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](https://github.com/ardatan/whatwg-node)
(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`](https://github.com/ardatan/whatwg-node) 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`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returning “another
standard” [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/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` ](https://github.com/ardatan/whatwg-node).

You simply provide your request handler that has a single
[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) parameter and expects you to
return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/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.

{/* prettier-ignore */}

```ts
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](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](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](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.

```tsx
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.

{/* prettier-ignore */}

```tsx
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 Node.js 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](https://github.com/ardatan/whatwg-node)!