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.
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
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Many (/users, /users/:id/posts, etc.) | Single (/graphql) |
| Client flexibility | Server decides response shape | Client picks fields |
| Over/under-fetching | Common | Rare |
| Caching | Easy (HTTP cache) | Harder (needs client-side cache like Apollo) |
| Learning curve | Low | Higher |
| Mobile apps | Fine | Great — thin networks love minimal payloads |
| Simple CRUD | REST is simpler | GraphQL 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.
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 dataloaderCurrent 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:
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:
# 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.xInside 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.
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:
{ 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
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:
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:
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:
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
EXPLAIN ANALYZE.__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.
A self-managed Domain India VPS runs Apollo Server, Hasura and WebSocket subscriptions with full root access.
View VPS plans