Implementing GraphQL APIs with JavaScript

Implementing GraphQL APIs with JavaScript

I spent years building REST APIs before I finally gave GraphQL a real try, and I’ll admit I was skeptical — it felt like extra ceremony for something REST already did fine. Then I built an API for a dashboard with a dozen deeply nested resources, and REST’s over-fetching and under-fetching problems finally became painful enough that I understood exactly why GraphQL exists. This is everything I’ve learned implementing GraphQL APIs with JavaScript, from the fundamentals to production-grade patterns.

What GraphQL Actually Solves

REST gives you fixed endpoints that return fixed shapes of data. If my frontend needs a user’s name and their last three orders, but the /users/:id endpoint doesn’t include orders, I either make a second request or bloat that endpoint for everyone who doesn’t need orders.

GraphQL flips this: the client describes exactly the shape of data it wants in a single query, and the server resolves exactly that — no more, no less.

query {
  user(id: "1") {
    name
    orders(limit: 3) {
      id
      total
    }
  }
}

One request, exactly the fields I asked for, nested resources included.

Core Building Blocks

A GraphQL API in JavaScript is built from three core pieces:

  1. Schema — a strongly typed contract describing what data is available.
  2. Resolvers — functions that fetch the actual data for each field in the schema.
  3. Server — the HTTP layer that parses incoming queries, validates them against the schema, and executes resolvers.

Setting Up a Basic GraphQL Server with Apollo Server

I typically reach for Apollo Server on Node.js — it’s mature and well documented. Here’s a minimal working example:

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

// 1. Define the schema (type definitions)
const typeDefs = `#graphql
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

// 2. Sample data
const books = [
  { title: 'The Pragmatic Programmer', author: 'Andy Hunt' },
  { title: 'Clean Code', author: 'Robert Martin' },
];

// 3. Resolvers map schema fields to actual data
const resolvers = {
  Query: {
    books: () => books,
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server running at ${url}`);

Running this and querying { books { title author } } in the GraphQL Playground returns:

{
  "data": {
    "books": [
      { "title": "The Pragmatic Programmer", "author": "Andy Hunt" },
      { "title": "Clean Code", "author": "Robert Martin" }
    ]
  }
}

Query Arguments and Mutations

Read operations use Query; writes use Mutation. I always define these explicitly since GraphQL doesn’t infer intent from HTTP verbs like REST does.

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

  type Query {
    book(id: ID!): Book
  }

  type Mutation {
    addBook(title: String!, author: String!): Book
  }
`;

let books = [{ id: '1', title: 'Clean Code', author: 'Robert Martin' }];

const resolvers = {
  Query: {
    book: (_, { id }) => books.find((b) => b.id === id),
  },
  Mutation: {
    addBook: (_, { title, author }) => {
      const newBook = { id: String(books.length + 1), title, author };
      books.push(newBook);
      return newBook;
    },
  },
};

Calling the mutation:

mutation {
  addBook(title: "Refactoring", author: "Martin Fowler") {
    id
    title
  }
}

returns:

{ "data": { "addBook": { "id": "2", "title": "Refactoring" } } }

How Resolvers Actually Execute Internally

This part clicked for me once I realized GraphQL execution is essentially a tree walk. When a query comes in, the server:

  1. Parses the query string into an AST (abstract syntax tree).
  2. Validates it against the schema — unknown fields or wrong types get rejected before any resolver runs.
  3. Executes it by walking the tree field-by-field, calling the matching resolver for each field, passing down the parent result as the first argument.

This is why nested resolvers receive four arguments: (parent, args, context, info). parent is the result of the resolver one level up — that’s how user.orders knows which user’s orders to fetch.

const resolvers = {
  Query: {
    user: (_, { id }, context) => context.db.getUser(id),
  },
  User: {
    orders: (parent, _, context) => context.db.getOrdersByUserId(parent.id),
  },
};

Because resolvers run independently per field, GraphQL servers execute sibling fields concurrently by default (when they return Promises), which is one reason nested queries can still be fast.

The N+1 Problem and DataLoader

The most common performance mistake I made early on: if a query asks for 50 users and each user’s orders field triggers its own database call, that’s 51 queries for one request. I now always use DataLoader to batch and cache these:

import DataLoader from 'dataloader';

const orderLoader = new DataLoader(async (userIds) => {
  const orders = await db.getOrdersForUsers(userIds); // one batched query
  return userIds.map((id) => orders.filter((o) => o.userId === id));
});

const resolvers = {
  User: {
    orders: (parent) => orderLoader.load(parent.id),
  },
};

DataLoader batches all .load() calls made within the same event loop tick into a single call, cutting 51 queries down to 2.

Consuming a GraphQL API from the Frontend

On the client side, I use fetch directly for simple cases, or Apollo Client / urql for anything with caching needs.

async function fetchBooks() {
  const response = await fetch('http://localhost:4000/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `query { books { title author } }`,
    }),
  });

  const { data } = await response.json();
  console.log(data.books);
}

Authentication in GraphQL

Unlike REST, there’s no per-endpoint middleware — auth typically happens in the context function, which runs once per request and is passed to every resolver.

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    const user = await verifyToken(token);
    return { user };
  },
});

Then inside a resolver:

Mutation: {
  addBook: (_, args, context) => {
    if (!context.user) throw new Error('Unauthorized');
    // proceed
  },
},

Best Practices

  • Design the schema around what clients need, not around your database tables.
  • Use DataLoader for anything resolving relational data to avoid N+1 queries.
  • Set query complexity/depth limits — an unbounded nested query is a real denial-of-service vector.
  • Version through schema evolution (deprecate fields), not through new endpoints.
  • Return meaningful, typed errors instead of generic 500s.

Common Mistakes

  • Treating every schema field as a 1:1 database column, leading to a leaky, hard-to-evolve schema.
  • Forgetting that GraphQL over HTTP still needs rate limiting — one query can hide unbounded work.
  • Not validating input arguments at the resolver level, even though the schema enforces basic types.

Security Considerations

Because clients can shape arbitrarily deep and wide queries, I always add query depth limiting and cost analysis (e.g., graphql-depth-limit, graphql-query-complexity) in production. I also disable introspection and the GraphQL Playground in production environments to avoid exposing my full schema to the public.

GraphQL vs REST

AspectRESTGraphQL
EndpointsMany, resource-basedSingle endpoint
Data shapeFixed per endpointClient-defined per query
Over/under-fetchingCommonAvoided by design
VersioningNew endpoint versionsSchema evolution (deprecation)
CachingNative HTTP cachingRequires client-side caching (Apollo, urql)
Learning curveLowerHigher (schema, resolvers, execution model)

FAQs

Does GraphQL replace REST entirely? Not necessarily — I still use REST for simple, cacheable, resource-based APIs, and GraphQL where clients need flexible, nested data.

Is GraphQL slower than REST? Not inherently — but poorly optimized resolvers (the N+1 problem) can make it slower if you’re not careful.

Do I need Apollo Server specifically? No, alternatives like graphql-yoga, express-graphql, and Mercurius (for Fastify) all work well too.

Can GraphQL work with any database? Yes — GraphQL is transport/schema layer agnostic; resolvers can call SQL, NoSQL, REST APIs, or anything else.

Summary and Key Takeaways

Implementing GraphQL with JavaScript taught me to think in terms of a typed schema and a resolver graph instead of a list of endpoints. The execution model — parse, validate, then walk the tree calling resolvers — explains almost every performance and security consideration you’ll run into, especially the N+1 problem. Once that model clicked, GraphQL stopped feeling like ceremony and started feeling like the right tool for APIs with genuinely nested, client-driven data needs.

References

Total
1
Shares

Leave a Reply

Previous Post
Implementing Geolocation Services with JavaScript

Implementing Geolocation Services with JavaScript

Next Post
Implementing WebAssembly with JavaScript

Implementing WebAssembly with JavaScript

Related Posts