ScreenshotNeo

BlogHow-to

How to Scaffold a GraphQL Server

Build a locally queryable GraphQL API with a schema, resolvers, and an HTTP server. Compare Apollo Server, NestJS, and GraphQL Yoga, then prepare the scaffold for production.

By the ScreenshotNeo team4 October 202610 min read

To scaffold a GraphQL server, create a schema that describes the fields clients can query, implement resolvers that return those fields, and connect both to an HTTP server. For a small Node.js service, Apollo Server is a direct starting point. Use NestJS when your application already follows Nest’s module structure or needs its code-first or schema-first workflow. Use GraphQL Yoga when you want a compact GraphQL-over-HTTP setup.

This guide builds a runnable Apollo Server in JavaScript, then covers TypeScript, cURL, Python, and Node.js client requests, framework choices, production considerations, and common setup errors. The examples target Node.js; the framework and deployment target can change the setup.

1. What a GraphQL scaffold needs

A working server has four basic parts:

  • GraphQL implementation: the package that parses and executes GraphQL operations.
  • HTTP integration: accepts requests and passes operations to GraphQL.
  • Schema: declares the types and fields clients may query.
  • Resolvers: provide the behavior and data for fields.

Apollo’s getting-started documentation puts the schema at the center: “Every GraphQL server (including Apollo Server) uses a schema to define the structure of data that clients can query.” Apollo Server handles HTTP requests and executes operations; the graphql package supplies GraphQL parsing and execution algorithms. See the Apollo Server getting-started guide.

2. Scaffold a minimal Apollo Server

Prerequisites

Apollo’s current getting-started guide requires Node.js v20.0.0 or newer. Check your installed version and create a project:

node --version
mkdir graphql-server
cd graphql-server
npm init --yes
npm pkg set type=module

Install Apollo Server and GraphQL:

npm install @apollo/server graphql

Create the server

Create index.js with a schema, some sample data, resolvers, and an HTTP listener. This example uses Apollo’s standalone server integration and an in-memory list so it runs without a database.

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const books = [
  { id: '1', title: 'The Hobbit', author: 'J. R. R. Tolkien' },
  { id: '2', title: 'Kindred', author: 'Octavia E. Butler' },
];

const typeDefs = `#graphql
  type Book {
    id: ID!
    title: String!
    author: String!
  }

  type Query {
    books: [Book!]!
    book(id: ID!): Book
  }
`;

const resolvers = {
  Query: {
    books: () => books,
    book: (_parent, { id }) => books.find((book) => book.id === id) ?? null,
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`GraphQL server ready at ${url}`);

Start the process:

node index.js

The standalone integration logs the server URL, normally http://localhost:4000/. Keep the process running while you make a request. The sample data resets whenever the process restarts; replace it with a database or service for persistent data.

Run a query with cURL

GraphQL requests are commonly sent as HTTP POST requests with a JSON body containing a query string. Quote the JSON carefully in the shell:

curl http://localhost:4000/ \
  -H 'content-type: application/json' \
  --data-binary '{"query":"query { books { id title author } }"}'

To look up one book:

curl http://localhost:4000/ \
  -H 'content-type: application/json' \
  --data-binary '{"query":"query BookById($id: ID!) { book(id: $id) { id title author } }","variables":{"id":"1"}}'

Expected data has a top-level data object. A missing book returns book: null, because that field is nullable in the schema. A field declared with ! is non-null; returning null for it causes GraphQL to report an execution error and propagate null to the nearest nullable parent.

3. Call the server from Python and Node.js

Python client

Install the HTTP client package, then send the operation and variables as JSON:

python -m pip install requests
import requests

endpoint = "http://localhost:4000/"
query = "query BookById($id: ID!) { book(id: $id) { id title author } }"
response = requests.post(
    endpoint,
    json={"query": query, "variables": {"id": "1"}},
    timeout=10,
)
response.raise_for_status()
result = response.json()
if "errors" in result:
    raise RuntimeError(result["errors"])
print(result["data"]["book"])

Node.js client

Node.js 20 includes fetch. This client sends the same operation:

const query = `query BookById($id: ID!) {
  book(id: $id) { id title author }
}`;

const response = await fetch('http://localhost:4000/', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ query, variables: { id: '1' } }),
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}

const result = await response.json();
if (result.errors) {
  throw new Error(JSON.stringify(result.errors));
}
console.log(result.data.book);

HTTP success and GraphQL success are distinct: GraphQL can return an HTTP response with an errors array, sometimes alongside partial data. Clients should inspect both the HTTP status and the GraphQL response body.

4. Add fields, arguments, and mutations deliberately

The scaffold demonstrates a query and an argument. Add mutations when clients need to change state, and define input types to make accepted values explicit. For example, an in-memory create operation could look like this:

// Add to typeDefs:
// input AddBookInput { title: String!, author: String! }
// type Mutation { addBook(input: AddBookInput!): Book! }

// Add alongside Query in resolvers:
// Mutation: {
//   addBook: (_parent, { input }) => {
//     const book = { id: String(books.length + 1), ...input };
//     books.push(book);
//     return book;
//   },
// },

Uncomment the input and mutation definitions inside the schema template, and the resolver inside the resolver map to enable the example. In a real application, validate input and authorization in the resolver or the application service it calls. Do not treat a schema’s types as authorization rules or as a substitute for input validation.

For lists backed by a database, plan pagination before a collection grows large. Also consider how nested resolvers fetch related records: resolving each row with a separate database query can create an N+1 query pattern. Batching or request-scoped data loaders can reduce repeated lookups, but their cache scope and authorization behavior need to match the request.

5. Choose Apollo Server, NestJS, or GraphQL Yoga

Option Good fit Schema workflow HTTP and integration notes
Apollo Server A small standalone JavaScript or TypeScript GraphQL service, or an app using one of its documented integrations. Schema and resolvers are explicit parts of the getting-started flow. Its docs cover Node.js frameworks and serverless environments; choose the integration that matches the host.
NestJS GraphQL An existing NestJS app or a project that benefits from Nest’s module structure. Code-first derives the schema from TypeScript decorators and classes; schema-first starts from GraphQL SDL. Nest documents Apollo Server and Mercurius drivers. Install and configure for the selected driver and Nest version.
GraphQL Yoga v5 A compact GraphQL-over-HTTP service or a setup where Yoga’s schema and platform options fit. Supports multiple schema-building approaches. The quick start installs graphql-yoga and graphql, creates a Yoga instance, and connects its handler to Node’s HTTP server at /graphql.

Decide based on the application you already have, whether you want schema-first SDL or code-first TypeScript, and where the service will run. There is no universally best choice for every project. Consult the primary setup docs for Apollo Server, NestJS GraphQL, and GraphQL Yoga.

Compact Yoga wiring example

Yoga’s documented v5 quick start uses the Yoga handler with Node’s HTTP server. Install its packages with npm install graphql-yoga graphql. A minimal shape is:

import { createServer } from 'node:http';
import { createSchema, createYoga } from 'graphql-yoga';

const yoga = createYoga({
  schema: createSchema({
    typeDefs: `type Query { hello: String! }`,
    resolvers: { Query: { hello: () => 'Hello, GraphQL!' } },
  }),
});

const server = createServer(yoga);
server.listen(4000, () => {
  console.log('GraphQL Yoga ready at http://localhost:4000/graphql');
});

Use the package version and options documented for your project rather than copying framework configuration across major versions. See the Yoga documentation for schema alternatives and HTTP setup details.

NestJS schema workflow

NestJS lets a team choose code-first or schema-first rather than requiring one workflow. In code-first, TypeScript classes and decorators generate the GraphQL schema. In schema-first, the team authors SDL and configures Nest to load it. Nest also documents Apollo and Mercurius drivers. Follow the current NestJS GraphQL quick start for the packages and configuration matching the chosen driver; it is better to start from its version-aware setup than combine its configuration with Apollo’s standalone example.

6. Move from local scaffold to production

A locally queryable server is a starting point, not a production security plan. Before exposing the endpoint, decide who can call it, which operations are permitted, and how much work a request can trigger. Yoga’s production guidance discusses these controls and operational choices.

  • Private APIs: if clients are controlled, persisted operations can restrict execution to operations registered by the developer.
  • Public APIs: consider query-cost controls such as maximum depth, directives, and alias limits or other workload-appropriate limits. GraphQL’s ability to request nested fields can make a small-looking request expensive.
  • Authentication and authorization: authenticate at the boundary and enforce access for the relevant records and fields in application logic. Hiding a browser IDE is not a security strategy.
  • Input and resource limits: validate mutations, set sensible request limits and timeouts, and consider the load on downstream services.
  • Caching: response caching can reduce work against services or databases where responses are safely reusable. Choose cache keys and lifetime around identity, authorization, and data freshness.
  • Error reporting: send server-side failures to an external reporting service such as Sentry if that suits operations. Avoid exposing stack traces or secrets in public responses.
  • Deployment configuration: bind the service and choose host, port, proxy, TLS, environment variables, and process supervision according to the deployment platform.

Do not copy a development setting into production without understanding its effect. Keep credentials out of source control, and use the hosting platform’s supported secret configuration.

7. Troubleshooting

Symptom Likely cause Fix
ERR_MODULE_NOT_FOUND for an import Dependencies were not installed in this directory, or the command ran from a different project folder. Run npm install in the project root and confirm package.json and node_modules are there.
Syntax error at import or top-level await Node is interpreting the file as CommonJS or the runtime is too old. Use Node.js 20 or newer and set "type": "module" in package.json, as in the setup above.
Port already in use Another process is listening on port 4000. Stop that process or change the example’s port and send requests to the matching URL.
Connection refused The server process is not running, failed during startup, or the client used the wrong host or port. Read the server terminal output, start node index.js, and use the printed URL.
GraphQL validation error such as “Cannot query field” The requested field is not in the active schema, or its spelling/capitalization differs. Check the schema, restart if needed, and query only declared fields.
Resolver returns an unexpected null or non-null error The resolver did not find data, returned the wrong shape, or returned null for a non-null field. Check the resolver’s data path and decide whether the field should be nullable; keep schema guarantees aligned with actual data.
HTTP response is successful but result contains errors GraphQL execution can report operation errors in its JSON response, possibly with partial data. Inspect the response body and handle the errors array in clients, not only HTTP status.
Yoga request sent to the wrong path The documented quick start serves at /graphql, unlike the standalone Apollo example URL. Use the endpoint path configured by the framework and verify it in the startup log or configuration.
Framework driver or package mismatch Packages or configuration were copied from a different Nest or driver version. Follow the current Nest quick start for the chosen Apollo or Mercurius driver and installed Nest version.

8. Performance, reliability, and cost

The scaffold’s in-memory resolvers are for learning; process restarts lose changes, and they do not coordinate across multiple server instances. A persistent database or service is needed for durable application data. Network calls in resolvers also affect response time and reliability, so use appropriate downstream timeouts and error handling.

GraphQL makes it easy for clients to choose fields, but nested operations can fan out into many data lookups. Measure the operations your clients actually send, add batching where repeated lookups occur, and apply query-cost controls appropriate to whether the API is private or public. Caching can help where data and authorization permit safe reuse. There is no single benchmark or cost estimate that applies across frameworks and workloads; hosting and database costs depend on the resources and traffic of the deployed application.

9. Grow the scaffold into an application

Once the endpoint works, the next steps usually include persistence, validation, pagination, filtering, authentication, and tests. The Guild’s tutorial develops a Node.js and TypeScript Yoga server with Prisma and SQLite, then adds validation, pagination, and filtering. Treat it as a learning path; those dependencies are not required for every GraphQL service. See the GraphQL Yoga tutorial.

Or skip the browser setup

When your development workflow also needs screenshots of pages or GraphQL documentation, ScreenshotNeo is a website screenshot API and MCP server. A single GET request captures a URL as PNG, JPEG, WebP, or PDF. For example, capture a page with cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. It accepts cookie and consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server gives AI agents tools including take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does GraphQL require a database?

No. The server can resolve data from memory or another service. A database is needed only when the application requires persistent data or database-backed behavior.

Does GraphQL use GET or POST?

The examples use POST with a JSON body. GraphQL-over-HTTP implementations can support other request forms depending on their configuration; follow the server and client documentation for the endpoint you deploy.

Can I use TypeScript?

Yes. Apollo documents JavaScript and TypeScript starter paths, and NestJS supports a code-first workflow based on TypeScript classes and decorators as well as schema-first SDL.

Should I expose a GraphQL IDE publicly?

That is a deployment choice, not a substitute for protecting the API. Decide separately how clients are authorized and how expensive or unapproved operations are controlled.