Why build a serverless GraphQL API?

Building an API can feel intimidating. Tutorials often pile on dependencies before explaining what any of them do, which is enough to scare off developers coming from a front-end background. But underneath most of that complexity is a small, understandable set of tools. A read-only API is achievable with about 30 lines of code, basic JavaScript familiarity, and no server management at all.

The two big concepts here are GraphQL and serverless. GraphQL, developed at Facebook starting in 2012, treats data as a graph of related objects — a user has followers, who have tweets, and so on. Instead of hitting multiple purpose-built URLs the way you would with a REST API, a client sends a single request to the API endpoint describing exactly what data it wants, and the server shapes the response accordingly. Serverless is a slightly misleading name: there is still a server, but you never buy, provision, or scale it. You just pay for what you use (or, in the case of Netlify's free tier, nothing at all up to a monthly call limit).

This guide walks through building a "Hello World" API with Node.js, Express, and Netlify Functions. It's enough to understand the mechanics and deploy something real.

The toolchain

Three tools cover the whole pipeline. Node.js runs JavaScript on a server, which is familiar territory for most front-end developers. Express is a popular Node.js framework that handles routing and middleware. Netlify provides a free plan for serverless functions and a dev CLI to simulate the production environment locally.

There is a caveat worth noting: running the full Express server inside a serverless function is efficient enough for a demo but not for production scale. Every request boots the entire Express app from scratch. For serious serverless applications, the recommended pattern is one single-purpose function per endpoint.

Project setup

Create a new folder and run npm init. Then install the dependencies in one command:

npm i express express-graphql graphql body-parser serverless-http

That installs Express, the GraphQL core library and express-graphql helper, body-parser for JSON translation, and serverless-http, which wraps Express so it runs as a serverless function. Netlify Dev is a global CLI:

npm i -g netlify-cli

Two configuration files are required. At the project root, netlify.toml tells Netlify the build and start commands and points to the functions directory:

[build]


  # This command builds the site
  command = "npm run build"


  # This is the directory that will be deployed
  publish = "build"


  # This is where our functions are located
  functions = "functions"

Create a /functions folder and inside it a file called api.js. That file begins by pulling in dependencies:

const express = require("express");
const bodyParser = require("body-parser");
const expressGraphQL = require("express-graphql");
const serverless = require("serverless-http");

Then initialize Express and wrap it in serverless-http:

const app = express();
module.exports.handler = serverless(app);

The module.exports.handler line exposes the Express app to Netlify as the serverless function. Middleware comes next:

app.use(bodyParser.json());
app.use(
  "/",
  expressGraphQL({
    graphiql: true
  })
);

The body-parser layer translates request bodies to and from JSON. The express-graphql middleware handles the GraphQL logic, and graphiql: true enables an in-browser query playground for testing.

Defining the GraphQL schema

GraphQL needs a schema — a typed blueprint of the data it can serve. For a read-only API the schema must define a root query that runs at the API's top-level endpoint, say api.example.com/graphql. The generic object types are available from the installed GraphQL package:

const {
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString
} = require("graphql");

The schema itself looks like this:

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'HelloWorld',
    fields: () => ({ /* we'll put our response here */ })
  })
})

The query key is what associates the root query with this configuration. It points to a GraphQL object with two config values: name for documentation, and fields, a function that returns the data definitions for the response. Since it's a function, the object can reference variables and functions declared elsewhere in the file.

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: "HelloWorld",
    fields: () => ({
      message: {
        type: GraphQLString,
        resolve: () => "Hello World",
      },
    }),
  }),
});

There is a single field here: message. Its type is GraphQLString. The resolve function is what the server executes to produce a value, so all it does is return "Hello World". In a more complex application, this would be where a database query happens. Update the Express configuration so the middleware is aware of the schema:

app.use(
  "/",
  expressGraphQL({
    schema: schema,
    graphiql: true
  })
);

Testing locally

From the project root, run netlify dev. Netlify Dev reads netlify.toml, bundles api.js, and serves everything at http://localhost:8888. Visiting that bare root URL returns a 404 because serverless functions are served from a different path:

Go to localhost:8888/.netlify/functions/api and the GraphiQL interface loads:

Now, that’s more like it!

Enter a GraphQL query in the left pane:

{
  message
}

Run it and the server responds with the message field from the schema:

{
  "data": {
    "message": "Hello World"
  }
}

Pretty URLs and deployment

Typing /.netlify/functions/api every time is not pleasant. Netlify supports a simple redirect rule built on a file called _redirects at the project root, no extension needed:

/api /.netlify/functions/api 200!

That line maps any request to yoursite.com/api to the serverless function while returning HTTP status 200. Hosting the source in a GitHub repo is the easiest path: connect the repo to Netlify and each push triggers a build and deployment automatically. The Netlify dashboard surfaces function logs if anything goes wrong.

The complete project source is public on GitHub. The schema and resolve functions established here extend directly to any data source — a database, a design-token store, or a personal API. The remarkable part is how little infrastructure knowledge it takes to get there.