,

GraphQL: A Modern Approach to APIs

GraphQL is a query language for APIs that provides a flexible and efficient way to fetch data compared to traditional REST APIs. Developed by Facebook in 2012 and open-sourced in 2015, GraphQL is a powerful query language and runtime for APIs, developed by Facebook in 2012 and open-sourced in 2015. It emerged as a response to the limitations of RESTful APIs, particularly in handling complex data requirements for modern applications. Unlike REST, which relies on fixed endpoints that return predefined responses, GraphQL provides a flexible approach where clients can specify the exact data they need in a single request.

At its core, GraphQL operates through a type system where developers define schemas that outline the structure of data available. Clients send queries using GraphQL’s declarative syntax, specifying fields and nested relationships, and the server responds with precisely the requested data—nothing more, nothing less. This eliminates over-fetching (retrieving unnecessary data) and under-fetching (requiring multiple requests to gather related information), both of which are common challenges in RESTful architectures.

GraphQL’s ability to aggregate data from multiple sources in a single request makes it particularly useful for applications with complex data needs, such as social media platforms, e-commerce sites, and real-time dashboards. It also enables efficient development of mobile and web applications by reducing the number of API calls, improving performance, and allowing front-end teams to work independently of back-end changes.

While GraphQL introduces flexibility and efficiency, it also requires careful schema design, caching strategies, and security considerations to prevent excessive queries. However, for organizations seeking a modern, adaptable API solution, GraphQL is an excellent alternative to traditional REST architectures.

File Structure in a GraphQL Project

A well-organized GraphQL project typically follows a modular structure to separate concerns and ensure maintainability. The most common file structure includes:

  • schema.graphql or schema.ts – Defines the types, queries, and mutations.
  • Resolvers (resolvers/) – Contains functions that handle queries and mutations.
  • Type Definitions (types/) – Stores modular GraphQL types for better scalability.
  • Data Sources (datasources/) – Manages integration with databases, REST APIs, or other services.
  • Context (context.ts) – Manages shared logic such as authentication and user state.
  • Server (server.ts or index.ts) – Configures and runs the GraphQL server, typically using Apollo Server or Express.

By structuring GraphQL projects in a modular way, developers can ensure scalability, readability, and easier debugging. This separation of concerns makes it simpler to manage queries, mutations, and data fetching logic independently.

How GraphQL Works: Schema, Queries, and Resolvers

GraphQL operates on a strongly-typed schema that defines the structure of available data and how clients can interact with it. The schema serves as the foundation, outlining object types, fields, and their relationships. It also specifies three core operation types: queries, mutations, and subscriptions.

  1. Schema and Type System – The schema is written using GraphQL’s type system, defining types (e.g., User, Post), their fields, and the relationships between them. This ensures that API requests are validated against a predictable data structure. Types can be scalar (e.g., String, Int, Boolean) or complex (custom objects, lists, or enums).
  2. Queries, Mutations, and Subscriptions – Queries allow clients to fetch specific data by specifying fields they need, reducing over-fetching. Mutations handle data modifications such as creating, updating, or deleting records. Subscriptions enable real-time updates by maintaining a persistent connection with the server, making them ideal for live data scenarios like chat applications or stock price tracking.
  3. Resolvers and Data Sources – Resolvers are functions that execute queries and mutations by retrieving data from various sources such as databases, APIs, or third-party services. Each field in a schema has a corresponding resolver function that determines how data is fetched or manipulated. This decoupling of schema and data logic enables flexible integrations with multiple backends.

By combining a schema-driven approach with a powerful query system and modular resolvers, GraphQL provides a more efficient and flexible alternative to traditional REST APIs, offering precise data retrieval, real-time capabilities, and streamlined API interactions.

How Resolver Functions Work in GraphQL

Resolvers are functions responsible for fetching the data for each field in a GraphQL query. When a query is executed, each field in the query tree can have its own resolver, allowing for granular data fetching. If a field requires its own independent request (e.g., fetching related data from an external API or another database table), the resolver ensures that it is retrieved separately. When a client sends a GraphQL query, the resolver functions determine how to fetch and return the requested data. The execution process follows these steps:

  • Query Execution Begins – The GraphQL server receives the client request and validates it against the schema.
  • Root Resolver Matching – The server identifies which resolver functions correspond to the requested fields in the query.
  • Resolver Execution – Each resolver function runs independently, retrieving data from databases, APIs, or other sources.
  • Field-Level Resolution – GraphQL processes each field separately, allowing nested queries to fetch related data efficiently.
  • Data Aggregation – Once all resolvers execute, GraphQL combines their results into a structured JSON response.
  • Response Sent to Client – The server returns only the requested fields, ensuring optimized data fetching.

This resolver-driven execution ensures flexibility, enabling efficient data retrieval while reducing over-fetching and under-fetching compared to traditional REST APIs.

Setting Up a Simple GraphQL API

To set up a basic GraphQL API, we use a Node.js environment with Apollo Server and Express. This setup involves defining a schema, implementing resolvers, and configuring a server to handle queries, mutations, and subscriptions. Below is a minimal GraphQL API for managing a list of users.

Install dependencies

npm init -y
npm install apollo-server express graphql

Define the schema

schema.graphql
type User {
    id: ID!
    name: String!
    email: String
    age: Int
}

type Query {
    users: [User!]!
    user(id: ID!): User
}

type Mutation {
    addUser(name: String!, age: Int): User!
}

type Subscription {
    userAdded: User
}

Implement resolvers

resolvers.js
import {User} from "./types";

const USERS: User[] = [];

async function fetchEmail(user: User) {
    console.log('Fetching from some place')
    return `${user.name}@email.com`
}

export const resolvers = {
    Query: {
        users: () => USERS,
    },
    Mutation: {
        addUser: (_, { name, age }, { pubsub }) => {
            const newUser = { id: USERS.length + 1 + '', name, age };
            USERS.push(newUser);

            console.log("🔔 Publishing event:", newUser);
            pubsub.publish('USER_ADDED', { userAdded: newUser });
            return newUser;
        }
    },
    Subscription: {
        userAdded: {
            subscribe: (_, __, { pubsub }) => {
                console.log("New subscriber connected!");
                return pubsub.asyncIterator(['USER_ADDED']);
            }
        }
    },
    User: {
        email: async (user: User) => fetchEmail(user),
    }
};

Setup the server

server.js
import { ApolloServer, PubSub } from 'apollo-server';
import { loadSchemaSync } from '@graphql-tools/load';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { printSchema } from 'graphql';
import {resolvers} from "./resolvers";


const pubsub = new PubSub();

const schema = loadSchemaSync('./schema.graphql', {
    loaders: [new GraphQLFileLoader()]
});
const typeDefs = printSchema(schema);

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: () => ({ pubsub }),
    subscriptions: {
        path: "/subscriptions",
    },
});

server.listen().then(({ url, subscriptionsUrl }) => {
    console.log(`Server ready at ${url}`);
    console.log(`Subscriptions ready at ${subscriptionsUrl}`);
});

Once the server is running, we can test the API using GraphQL Playground or Postman.

  • Query (Fetching Users)
graphql
query {
  users {
    id
    name
    email
  }
}
  • Mutation (Adding a User)
graphql
mutation {
  addUser(name: "John Doe", age: 20) {
    id
    name
    email
  }
}
  • Subscription (Listening for New Users)
graphql
subscription {
  userAdded {
    id
    name
    email
  }
}

With this setup, the API allows querying user data, adding new users via mutations, and receiving real-time notifications through subscriptions. This demonstrates how GraphQL enables a flexible and efficient API design suitable for dynamic applications.

Check my Github for working example

Leave a Reply

Your email address will not be published. Required fields are marked *