GraphQL Tutorial for Beginners: Build Your First API 2026
Tutorials

GraphQL Tutorial for Beginners: Build Your First API 2026

13 min read
107 Views
Share:

Why GraphQL is worth learning in 2026

After working with REST APIs for years, the first time I built something with GraphQL I felt like I had been carrying unnecessary weight. No more overfetching data you do not need. No more underfetching that forces three sequential requests just to render a single page. No more versioning headaches when your front-end team needs slightly different data shapes.

GraphQL, developed internally at Facebook and open-sourced in 2015, has become a first-class citizen in API development. GitHub, Shopify, Twitter, and thousands of other companies use GraphQL in production. In 2026, understanding GraphQL is expected knowledge for any full-stack developer working with modern web applications.

GraphQL vs REST: understanding the difference

With REST, the server defines the data structure and the client adapts. With GraphQL, the client describes exactly what it needs and the server delivers precisely that. This client-driven approach solves the two biggest pain points of REST: overfetching (receiving more data than needed) and underfetching (requiring multiple round trips to get all the data for one screen).

Imagine you are building a profile page that shows a user's name, their last three posts, and each post's comment count. With REST you might call GET /users/1, GET /users/1/posts?limit=3, and then GET /posts/456/comments/count for each post. With GraphQL, one request gets everything.

Setting up your first GraphQL server

We will build a GraphQL API for a blog application using Node.js and Apollo Server 4, the most popular GraphQL server library for JavaScript.

# Create project and install dependencies
mkdir graphql-blog-api && cd graphql-blog-api
npm init -y
npm install @apollo/server graphql
npm install -D nodemon

# Create entry file
touch index.js
// index.js — Complete working GraphQL server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// Type definitions (schema)
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    body: String!
    published: Boolean!
    author: User!
    comments: [Comment!]!
  }

  type Comment {
    id: ID!
    text: String!
    author: User!
    post: Post!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts(published: Boolean): [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createPost(title: String!, body: String!, authorId: ID!): Post!
    publishPost(id: ID!): Post!
    deletePost(id: ID!): Boolean!
  }
`;

// In-memory data for this tutorial
const users = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com' },
];

let posts = [
  { id: '1', title: 'Getting Started with GraphQL', body: 'GraphQL is amazing...', published: true, authorId: '1' },
  { id: '2', title: 'Advanced Apollo Patterns', body: 'After using Apollo for years...', published: false, authorId: '1' },
  { id: '3', title: 'REST vs GraphQL', body: 'Both have their place...', published: true, authorId: '2' },
];

const comments = [
  { id: '1', text: 'Great article!', authorId: '2', postId: '1' },
  { id: '2', text: 'Very helpful, thanks.', authorId: '1', postId: '3' },
];

// Resolvers — the functions that fetch data
const resolvers = {
  Query: {
    users: () => users,
    user: (_, { id }) => users.find(u => u.id === id),
    posts: (_, { published }) => {
      if (published === undefined) return posts;
      return posts.filter(p => p.published === published);
    },
    post: (_, { id }) => posts.find(p => p.id === id),
  },

  Mutation: {
    createPost: (_, { title, body, authorId }) => {
      const post = { id: String(posts.length + 1), title, body, published: false, authorId };
      posts.push(post);
      return post;
    },
    publishPost: (_, { id }) => {
      const post = posts.find(p => p.id === id);
      if (!post) throw new Error('Post not found');
      post.published = true;
      return post;
    },
    deletePost: (_, { id }) => {
      const index = posts.findIndex(p => p.id === id);
      if (index === -1) return false;
      posts.splice(index, 1);
      return true;
    },
  },

  // Relationship resolvers
  User: {
    posts: (parent) => posts.filter(p => p.authorId === parent.id),
  },

  Post: {
    author: (parent) => users.find(u => u.id === parent.authorId),
    comments: (parent) => comments.filter(c => c.postId === parent.id),
  },

  Comment: {
    author: (parent) => users.find(u => u.id === parent.authorId),
    post: (parent) => posts.find(p => p.id === parent.postId),
  },
};

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

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

Add "type": "module" to your package.json and run node index.js. Open http://localhost:4000 to access Apollo Sandbox, a built-in GraphQL playground.

Writing your first queries

With the server running, you can explore data with GraphQL queries. The power lies in requesting exactly the fields you need.

# Fetch all published posts with author name only
query GetPublishedPosts {
  posts(published: true) {
    id
    title
    author {
      name
    }
  }
}

# Fetch a single user with their posts and comment counts
query GetUserProfile {
  user(id: "1") {
    name
    email
    posts {
      id
      title
      published
      comments {
        id
        text
      }
    }
  }
}

# Create a new post
mutation CreatePost {
  createPost(
    title: "My GraphQL Journey"
    body: "I started learning GraphQL and it changed how I build APIs..."
    authorId: "2"
  ) {
    id
    title
    published
    author {
      name
    }
  }
}

# Publish the post
mutation PublishPost {
  publishPost(id: "4") {
    id
    title
    published
  }
}

Arguments, variables, and fragments

Hardcoding values in queries is fine for exploration but not for real applications. GraphQL variables let you pass dynamic values cleanly, and fragments let you reuse field selections across multiple queries.

# Using variables (the right way to pass dynamic values)
query GetPost($postId: ID!) {
  post(id: $postId) {
    ...PostFields
    comments {
      text
      author {
        name
      }
    }
  }
}

# Fragment — reusable field set
fragment PostFields on Post {
  id
  title
  body
  published
  author {
    name
    email
  }
}

# Variables are sent separately as JSON:
# { "postId": "1" }

# Query with multiple operations and aliases
query Dashboard {
  publishedPosts: posts(published: true) {
    id
    title
  }
  draftPosts: posts(published: false) {
    id
    title
  }
}

Connecting to a real database

In production you will replace the in-memory arrays with real database calls. The resolver pattern makes this straightforward.

// Using Prisma ORM with PostgreSQL
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

const resolvers = {
  Query: {
    posts: async (_, { published }) => {
      return prisma.post.findMany({
        where: published !== undefined ? { published } : undefined,
        include: { author: true },
        orderBy: { createdAt: 'desc' },
      });
    },
    user: async (_, { id }) => {
      return prisma.user.findUnique({
        where: { id: Number(id) },
      });
    },
  },

  Mutation: {
    createPost: async (_, { title, body, authorId }) => {
      return prisma.post.create({
        data: { title, body, authorId: Number(authorId), published: false },
        include: { author: true },
      });
    },
  },
};

Comparison: REST vs GraphQL

AspectRESTGraphQL
Data fetchingFixed endpoints, fixed response shapeClient requests exactly what it needs
OverfetchingCommon (extra fields always returned)Eliminated by design
Multiple resourcesMultiple round tripsSingle request for all related data
Versioningv1, v2, v3 endpointsSchema evolution, deprecation
CachingHTTP caching is straightforwardRequires Apollo or query caching
Learning curveLower initial barrierSteeper but pays dividends quickly
ToolingPostman, curl, OpenAPIApollo Studio, GraphiQL, Postman

Common errors and solutions

Error: "Cannot return null for non-nullable field"

This happens when a resolver returns null or undefined for a field marked as required (with ! in the schema). Check your data source: the field might not exist in your database for that record, or your resolver is not returning the right data shape. If the field can legitimately be null, remove the ! from your type definition.

Error: "Field 'X' doesn't exist on type 'Y'"

Your query is requesting a field that is not defined in the schema. This is actually GraphQL working as intended — it catches field name typos at query validation time. Double-check the field name against the schema, or run an introspection query in Apollo Sandbox to see all available fields.

N+1 query problem

When fetching a list of posts, if each post fetches its author via a separate database query, you end up with N+1 queries (1 for the list, N for each author). The solution is DataLoader, which batches multiple requests into one. Install with npm install dataloader and create a loader per request to batch and cache database lookups.

Error: "Variable '$X' of type 'String' used in position expecting type 'ID!'"

GraphQL types are strict. If your schema defines an argument as ID!, you must pass it as a GraphQL ID variable, not String. Update your variable declaration: query MyQuery($id: ID!) instead of query MyQuery($id: String!).

CORS errors when connecting from a browser

When your GraphQL server and front-end run on different ports, you will hit CORS errors. Configure CORS in Apollo Server: pass { cors: { origin: 'http://localhost:3000' } } to startStandaloneServer. In production, replace with your actual domain.

Additional resources

J
Written by
Jesús García

Apasionado por la tecnologia y las finanzas personales. Escribo sobre innovacion, inteligencia artificial, inversiones y estrategias para mejorar tu economia. Mi objetivo es hacer que temas complejos sean accesibles para todos.

Share post:

Related posts

Comments

Leave a comment

Recommended Tools

The ones we use in our projects

Affiliate links. No extra cost to you.

Need technology services?

We offer comprehensive web development, mobile apps, consulting, and more.

Web Development Mobile Apps Consulting