DEV Community

Cover image for JavaScript runs everywhere, so should your servers - here is how
TheGuildBot for The Guild

Posted on • Edited on • Originally published at the-guild.dev

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

This article was published on Monday, August 22, 2022 by Arda Tanrikulu @ The Guild Blog

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 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()
Enter fullscreen mode Exit fullscreen mode

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'
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

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.

{/* prettier-ignore */}

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' })
Enter fullscreen mode Exit fullscreen mode

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
    })
  }
})
Enter fullscreen mode Exit fullscreen mode

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 */}

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)
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)