Practical GraphQL API Development - From Scratch

author

By Freecoderteam

Oct 24, 2025

5

image

Practical GraphQL API Development: From Scratch

GraphQL has revolutionized the way developers build APIs by offering a more efficient and flexible alternative to REST. Instead of rigid endpoints, GraphQL allows clients to specify exactly what data they need, reducing over-fetching and under-fetching. In this comprehensive guide, we'll walk through the process of building a GraphQL API from scratch, covering setup, schema design, resolver implementation, and best practices.

Table of Contents


Introduction to GraphQL

GraphQL is a query language for APIs, designed to provide a more efficient, powerful, and flexible alternative to REST. With GraphQL, clients describe exactly what data they need, and the server responds with only that data, no more and no less. This approach reduces unnecessary data transmission and allows for better performance, especially in mobile and real-time applications.

Key features of GraphQL include:

  • Schema-Driven APIs: GraphQL APIs are defined by a schema that specifies the structure of the data and the operations available.
  • Strong Typing: GraphQL uses a strongly typed schema to ensure consistent and predictable data.
  • Client-Side Querying: Clients can request only the data they need, reducing over-fetching and under-fetching.
  • Graph-Based Data Relationships: GraphQL naturally supports nested queries, making it easy to fetch related data in a single request.

In this tutorial, we'll build a simple GraphQL API that manages a basic todo list. We'll use Node.js, Express, and the Apollo Server library to set up the API.


Setting Up the Development Environment

To get started, we'll need the following tools:

  • Node.js: The runtime environment for server-side JavaScript.
  • npm or Yarn: Package managers for managing dependencies.
  • Apollo Server: A library for building GraphQL servers.

Step 1: Create a New Project

First, create a new directory for your project and initialize it with npm:

mkdir todo-graphql-api
cd todo-graphql-api
npm init -y

Step 2: Install Dependencies

We'll install the necessary packages for building a GraphQL API:

npm install apollo-server graphql

Step 3: Create the Entry Point

Create a file named index.js in your project directory:

// index.js
const { ApolloServer, gql } = require('apollo-server');

// Define a simple schema
const typeDefs = gql`
  type Query {
    hello: String
  }
`;

// Define resolvers
const resolvers = {
  Query: {
    hello: () => 'Hello, GraphQL!',
  },
};

// Create the Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });

// Start the server
server.listen().then(({ url }) => {
  console.log(`Server is running at ${url}`);
});

Step 4: Run the Server

Start the server by running:

node index.js

You should see the server running at http://localhost:4000. You can test it by navigating to http://localhost:4000/graphql, where the GraphQL Playground will be available.


Defining the GraphQL Schema

The GraphQL schema is the contract between the server and the client. It defines the structure of the data and the operations available. We'll start by defining a schema for a simple todo list.

Step 1: Define the Schema

Let's define types for Todo and Query:

// schema.js
const { gql } = require('apollo-server');

const typeDefs = gql`
  # Define the Todo type
  type Todo {
    id: ID!
    text: String!
    completed: Boolean!
  }

  # Define the Query type
  type Query {
    todos: [Todo!]!
    todo(id: ID!): Todo
  }

  # Define the Mutation type
  type Mutation {
    createTodo(text: String!): Todo
    updateTodo(id: ID!, completed: Boolean!): Todo
    deleteTodo(id: ID!): Todo
  }
`;

module.exports = typeDefs;

Explanation:

  • Todo: Represents a single todo item with id, text, and completed fields.
  • Query: Defines operations to fetch todos. todos returns all todos, and todo fetches a single todo by ID.
  • Mutation: Defines operations to create, update, and delete todos.

Step 2: Import the Schema

Update index.js to use the schema:

// index.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');

// Define resolvers
const resolvers = {
  Query: {
    todos: () => [],
    todo: (_, { id }) => todos.find(todo => todo.id === id),
  },
  Mutation: {
    createTodo: (_, { text }) => {
      const newTodo = { id: generateId(), text, completed: false };
      todos.push(newTodo);
      return newTodo;
    },
    updateTodo: (_, { id, completed }) => {
      const todo = todos.find(todo => todo.id === id);
      if (todo) {
        todo.completed = completed;
        return todo;
      }
      return null;
    },
    deleteTodo: (_, { id }) => {
      const todoIndex = todos.findIndex(todo => todo.id === id);
      if (todoIndex !== -1) {
        const [removedTodo] = todos.splice(todoIndex, 1);
        return removedTodo;
      }
      return null;
    },
  },
};

// Mock data
const todos = [];
const generateId = () => Math.random().toString(32).slice(-6);

// Create the Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });

// Start the server
server.listen().then(({ url }) => {
  console.log(`Server is running at ${url}`);
});

Explanation:

  • We've defined mock data and resolvers for the schema. The resolvers are functions that determine how to fetch or modify data for each field in the schema.

Implementing Resolvers

Resolvers are functions that determine how to fetch or modify data for each field in the schema. Let's implement the resolvers for our todo list.

Step 1: Complete the Resolvers

In the index.js file, update the resolvers to handle all operations:

// index.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');

// Mock data
const todos = [];
const generateId = () => Math.random().toString(32).slice(-6);

// Resolvers
const resolvers = {
  Query: {
    todos: () => todos,
    todo: (_, { id }) => todos.find(todo => todo.id === id),
  },
  Mutation: {
    createTodo: (_, { text }) => {
      const newTodo = { id: generateId(), text, completed: false };
      todos.push(newTodo);
      return newTodo;
    },
    updateTodo: (_, { id, completed }) => {
      const todo = todos.find(todo => todo.id === id);
      if (todo) {
        todo.completed = completed;
        return todo;
      }
      return null;
    },
    deleteTodo: (_, { id }) => {
      const todoIndex = todos.findIndex(todo => todo.id === id);
      if (todoIndex !== -1) {
        const [removedTodo] = todos.splice(todoIndex, 1);
        return removedTodo;
      }
      return null;
    },
  },
};

// Create the Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });

// Start the server
server.listen().then(({ url }) => {
  console.log(`Server is running at ${url}`);
});

Explanation:

  • Query Resolvers: todos returns all todos, and todo fetches a single todo by ID.
  • Mutation Resolvers: createTodo adds a new todo, updateTodo updates an existing todo, and deleteTodo removes a todo.

Connecting to a Data Source

For production use, we'll want to connect to a real data source like a database. Let's integrate with a simple in-memory database like json-server.

Step 1: Install json-server

Install json-server to simulate a REST API:

npm install json-server --save-dev

Step 2: Create a JSON File

Create a file named db.json in your project directory:

// db.json
{
  "todos": [
    { "id": "1", "text": "Learn GraphQL", "completed": false },
    { "id": "2", "text": "Build a GraphQL API", "completed": false }
  ]
}

Step 3: Update Resolvers to Use json-server

Update the resolvers in index.js to fetch data from json-server:

// index.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const axios = require('axios');

// JSON Server URL
const BASE_URL = 'http://localhost:3000';

// Resolvers
const resolvers = {
  Query: {
    todos: async () => {
      const { data } = await axios.get(`${BASE_URL}/todos`);
      return data;
    },
    todo: async (_, { id }) => {
      const { data } = await axios.get(`${BASE_URL}/todos/${id}`);
      return data;
    },
  },
  Mutation: {
    createTodo: async (_, { text }) => {
      const { data } = await axios.post(`${BASE_URL}/todos`, { text, completed: false });
      return data;
    },
    updateTodo: async (_, { id, completed }) => {
      const { data } = await axios.patch(`${BASE_URL}/todos/${id}`, { completed });
      return data;
    },
    deleteTodo: async (_, { id }) => {
      const { data } = await axios.delete(`${BASE_URL}/todos/${id}`);
      return data;
    },
  },
};

// Create the Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });

// Start the server
server.listen().then(({ url }) => {
  console.log(`Server is running at ${url}`);
});

Explanation:

  • Query Resolvers: Use axios to fetch todos from json-server.
  • Mutation Resolvers: Use axios to perform CRUD operations on the json-server API.

Step 4: Start json-server

Start json-server using the following command:

npx json-server --watch db.json

Now, your GraphQL API will interact with the json-server for data retrieval and modification.


Testing the API

To test our GraphQL API, we can use the built-in GraphQL Playground available at http://localhost:4000/graphql. Here are some example queries and mutations:

Query: Fetch All Todos

query {
  todos {
    id
    text
    completed
  }
}

Query: Fetch a Single Todo

query {
  todo(id: "1") {
    id
    text
    completed
  }
}

Mutation: Create a Todo

mutation {
  createTodo(text: "Write a blog post") {
    id
    text
    completed
  }
}

Mutation: Update a Todo

mutation {
  updateTodo(id: "1", completed: true) {
    id
    text
    completed
  }
}

Mutation: Delete a Todo

mutation {
  deleteTodo(id: "1") {
    id
    text
    completed
  }
}

Best Practices for GraphQL API Development

  1. Keep the Schema Simple: Start with a minimal schema and expand it as needed. Avoid over-engineering the schema.
  2. Use Strongly Typed Schemas: Always define types and fields with strong types to ensure consistent data.
  3. Optimize Resolvers: Use efficient queries and avoid unnecessary data fetching.
  4. Error Handling: Implement proper error handling in resolvers to provide meaningful feedback to clients.
  5. Pagination: For large datasets, implement pagination to avoid slow queries.
  6. Validation: Use input validation to ensure data integrity.
  7. Caching: Use GraphQL middleware or client-side caching to improve performance.
  8. Documentation: Keep your schema well-documented using GraphQL schema documentation tools.

Conclusion

In this tutorial, we walked through the process of building a GraphQL API from scratch, starting with the basics of schema design and moving on to resolver implementation and data source integration. We also covered best practices for maintaining a robust and efficient GraphQL API.

By following these steps, you can build powerful and flexible APIs that provide exactly the data clients need, improving performance and reducing complexity. Whether you're building a simple todo app or a complex enterprise application, GraphQL offers a modern and scalable approach to API development.

If you have any questions or need further assistance, feel free to reach out! Happy coding! 🚀


Feel free to ask if you need any additional details or modifications!

Share this post :

Subscribe to Receive Future Updates

Stay informed about our latest updates, services, and special offers. Subscribe now to receive valuable insights and news directly to your inbox.

No spam guaranteed, So please don’t send any spam mail.