GraphQL APIs

Building GraphQL APIs on Domain India: Apollo Server, Hasura, and Best Practices

By Domain India Team · DomainIndia EngineeringPublished 8 min read
Knowledge base article
Contents (8 sections)

GraphQL gives your web and mobile apps one endpoint where each client asks for exactly the data it needs. This guide helps you decide whether you need it, then shows two ways to build it and the traps to avoid in production.

Key takeaways

GraphQL lets clients ask for exactly the fields they need — no more over- or under-fetching. Use Apollo Server when you want custom resolvers in Node.js, or Hasura for an instant API over PostgreSQL. Batch database calls with DataLoader, cap query depth and complexity, and remember that subscriptions need WebSockets, which only a VPS provides on Domain India. This guide covers when GraphQL beats REST and how to run it on Domain India hosting.

GraphQL vs REST — when to pick which

FeatureRESTGraphQL
EndpointsMany (/users, /users/:id/posts, etc.)Single (/graphql)
Client flexibilityServer decides response shapeClient picks fields
Over/under-fetchingCommonRare
CachingEasy (HTTP cache)Harder (needs client-side cache like Apollo)
Learning curveLowHigher
Mobile appsFineGreat — thin networks love minimal payloads
Simple CRUDREST is simplerGraphQL is overkill

Pick GraphQL when:

  • Multiple clients (web + iOS + Android) need different fields
  • Deep nested data (user → posts → comments) where REST would need 3+ requests
  • Rapidly evolving frontend — backend doesn't need to deploy new endpoints

Stick with REST when:

  • Simple CRUD
  • HTTP caching is critical
  • Team is small and REST-experienced

Option A — Apollo Server (Node.js)

Full control, custom resolvers, flexible auth.

bash
mkdir gql-api && cd gql-api
npm init -y
npm pkg set type=module          # the example uses ES modules and top-level await
npm install @apollo/server graphql @as-integrations/express5 express dataloader

Current Apollo Server (version 5) ships the Express integration as a separate package: @as-integrations/express5 for Express 5, or @as-integrations/express4 if your app is still on Express 4.

server.js:

javascript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
import express from 'express';
import { PrismaClient } from '@prisma/client'; // or your own database layer

const prismaClient = new PrismaClient();

const typeDefs = `#graphql
  type User { id: ID! name: String! email: String! posts: [Post!]! }
  type Post { id: ID! title: String! author: User! }
  type Query {
    user(id: ID!): User
    posts: [Post!]!
  }
  type Mutation {
    createPost(title: String!, authorId: ID!): Post!
  }
`;

const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => db.user.findUnique({ where: { id } }),
    posts: async (_, __, { db }) => db.post.findMany(),
  },
  User: {
    posts: async (user, _, { db }) => db.post.findMany({ where: { authorId: user.id } }),
  },
  Post: {
    author: async (post, _, { db }) => db.user.findUnique({ where: { id: post.authorId } }),
  },
  Mutation: {
    createPost: async (_, args, { db }) => db.post.create({ data: args }),
  },
};

const apollo = new ApolloServer({ typeDefs, resolvers });
await apollo.start();

const app = express();
app.use('/graphql', express.json(), expressMiddleware(apollo, {
  context: async ({ req }) => ({ db: prismaClient, user: req.user }),
}));

const port = process.env.PORT || 4000;
app.listen(port, () => console.log(`GraphQL ready on port ${port}`));

Deploy on Domain India:

  • cPanel or DirectAdmin shared hosting: use the panel's "Setup Node.js App" tool, the same way as any Express API (see Deploy a Node.js app on shared hosting). Queries and mutations work; subscriptions and other long-running processes do not.
  • App Platform: Node.js is detected automatically and PostgreSQL is included on every plan. No WebSockets, so no subscriptions.
  • VPS: systemd and an nginx reverse proxy (see our Node.js Development articles); the only Domain India option for subscriptions.

Option B — Hasura (instant GraphQL over PostgreSQL)

Hasura reads your PostgreSQL schema and exposes a full GraphQL API — no resolvers to write.

Hasura runs as a Docker container, so it needs a VPS. Install Docker, then:

bash
# Use the current v2 image tag from Hasura's docs in place of v2.x.x
docker run -d --name hasura --restart unless-stopped \
  --add-host=host.docker.internal:host-gateway \
  -p 127.0.0.1:8080:8080 \
  -e HASURA_GRAPHQL_DATABASE_URL=postgres://user:[email protected]:5432/mydb \
  -e HASURA_GRAPHQL_ENABLE_CONSOLE=true \
  -e HASURA_GRAPHQL_ADMIN_SECRET='a-long-random-secret' \
  hasura/graphql-engine:v2.x.x

Inside a container, localhost is the container itself, so the database URL uses host.docker.internal to reach PostgreSQL on the VPS (PostgreSQL must listen on the Docker bridge address and allow it in pg_hba.conf). The port is bound to 127.0.0.1, so reach the console through an SSH tunnel (ssh -L 8080:localhost:8080 you@your-vps) and open http://localhost:8080/console. Log in with the admin secret, and every tracked table is queryable via GraphQL. Permissions, subscriptions and relationships are all set up in the console. Put nginx with HTTPS in front for public traffic, and turn the console off in production.

Hasura also offers a newer v3 engine (Hasura DDN) with a different setup; the example above uses v2.

Insight

Hasura is excellent for internal tools, because it removes most of the boilerplate. For customer-facing APIs, put it behind nginx with HTTPS, use JWT or webhook auth with role permissions, and add rate limiting.

The N+1 problem (and DataLoader fix)

Classic GraphQL trap. Query:

graphql
{ posts { title author { name } } }

Naive resolver runs:

  • 1 query for all posts
  • 1 query per post to get each author

100 posts = 101 queries. Disaster.

Fix: batch with DataLoader

javascript
import DataLoader from 'dataloader';

// Create loaders per request, in the context function, so cached
// results never leak between users
context: async ({ req }) => ({
  db: prismaClient,
  loaders: {
    user: new DataLoader(async (ids) => {
      const users = await prismaClient.user.findMany({ where: { id: { in: [...ids] } } });
      const byId = new Map(users.map(u => [u.id, u]));
      return ids.map(id => byId.get(id) ?? null);
    }),
  },
}),

// In the Post resolver:
author: (post, _, { loaders }) => loaders.user.load(post.authorId)

Now 100 posts = 2 queries (all posts, then all authors in one batch).

Authentication patterns

Three common approaches:

JWT in Authorization header:

javascript
context: async ({ req }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  const user = token ? jwt.verify(token, JWT_SECRET) : null;
  return { user, db };
}

Session cookie: use express-session middleware before Apollo.

Hasura JWT mode: Hasura validates JWT issued by your auth service (Auth0, Firebase Auth, custom). Claims map to PostgreSQL roles for row-level permissions.

Rate limiting

GraphQL's flexibility lets clients ask for deeply nested data — one query can become expensive. Defend:

Query complexity analysis (graphql-query-complexity), as an Apollo Server plugin:

javascript
import { GraphQLError } from 'graphql';
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';

const complexityPlugin = {
  async requestDidStart() {
    return {
      async didResolveOperation({ request, document, schema }) {
        const complexity = getComplexity({
          schema,
          query: document,
          variables: request.variables,
          estimators: [simpleEstimator({ defaultComplexity: 1 })],
        });
        if (complexity > 1000) {
          throw new GraphQLError(`Query too complex: ${complexity}`);
        }
      },
    };
  },
};

Depth limiting, plus both settings in the server config:

javascript
import depthLimit from 'graphql-depth-limit';

const apollo = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [complexityPlugin],
  validationRules: [depthLimit(7)],
});

Complexity and depth limits cap the cost of one query; limit the number of requests per client as well, in nginx or in your app.

Common pitfalls

N+1 queries overloading the database
Use DataLoader (per request) for every relation resolver, and check slow queries with EXPLAIN ANALYZE.
Exposing internal IDs
Sequential IDs let clients scrape your data. Use UUIDs or opaque, Relay-style global IDs.
No query complexity limit
One deeply nested query can ask for millions of rows. Always cap complexity and depth.
HTTP caching not working
Queries sent by POST bypass HTTP caches. Use a client cache such as Apollo Client, or persisted queries sent by GET.
Subscriptions on the wrong host
Subscriptions need WebSockets, which Domain India shared hosting and the App Platform don't support. Run them on a VPS with nginx proxying the WebSocket upgrade.
Introspection open in production
Anyone can query __schema to map your API. Apollo Server turns introspection off when NODE_ENV=production; set introspection: false explicitly to be sure.

FAQ

Can I run GraphQL on Domain India shared hosting?

Apollo Server can run on cPanel or DirectAdmin shared hosting through the Setup Node.js App tool, for queries and mutations only; subscriptions and background workers need a VPS. Hasura runs in Docker, so it needs a Domain India VPS.

Should I use GraphQL or gRPC for microservices?

gRPC suits service-to-service calls: binary, fast and strictly typed. GraphQL suits client-facing APIs, where flexible queries and a self-describing schema help frontend teams.

Does GraphQL replace REST completely?

No. Many teams use both: GraphQL for complex client queries, and REST for webhooks, file uploads and health checks.

How do I version a GraphQL API?

Usually you don't create versions. You add new fields and mark old ones with the @deprecated directive and a reason, then remove them once no client uses them, so the schema evolves without breaking clients.

What size VPS do I need for GraphQL subscriptions?

It depends on the number of open connections and how much each one does. Every connected client holds memory on the server, so load-test with realistic connection counts, watch memory use, and choose a plan with headroom.

Ready to build? Start small with the App Platform or the Node.js tool on cPanel hosting, and move to a VPS when you need Hasura or subscriptions.

Host your GraphQL API

A self-managed Domain India VPS runs Apollo Server, Hasura and WebSocket subscriptions with full root access.

View VPS plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app