JavaScript runs everywhere, so should your servers - here is how
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()
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;
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 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!
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 Beehiiv’s Terms of Service and Privacy Policy.