# Building GraphQL APIs on DomainIndia: Apollo Server, Hasura, and Best Practices
TL;DR
GraphQL lets clients ask for exactly the fields they need — no more over- or under-fetching. This guide covers when GraphQL beats REST, running Apollo Server (Node.js) or Hasura (instant GraphQL from PostgreSQL) on DomainIndia hosting, and avoiding the "N+1 query" trap.
## 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.
```bash
mkdir gql-api && cd gql-api
npm init -y
npm install @apollo/server graphql @as-integrations/express4 express
```
`server.js`:
```javascript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express4';
import express from 'express';
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 }),
}));
app.listen(4000, () => console.log('http://localhost:4000/graphql'));
```
Deploy on DomainIndia:
- Shared cPanel: "Setup Node.js App" — same pattern as any Express API
- VPS: systemd + nginx reverse proxy (see our [Node.js Development articles](https://domainindia.com/support/kb/category/dev-nodejs))
## Option B — Hasura (instant GraphQL over PostgreSQL)
Hasura reads your PostgreSQL schema and exposes a full GraphQL API — no resolvers to write.
Install on VPS:
```bash
# Docker-based (easiest)
docker run -d -p 8080:8080
-e HASURA_GRAPHQL_DATABASE_URL=postgres://user:pass@localhost/mydb
-e HASURA_GRAPHQL_ENABLE_CONSOLE=true
-e HASURA_GRAPHQL_ADMIN_SECRET=supersecret
hasura/graphql-engine:v2.36.0
```
Visit `http://your-vps:8080/console`, log in with the admin secret, and every table is now queryable via GraphQL. Permissions, subscriptions (real-time), derived relationships — all via UI.
Insight
Hasura is incredible for internal tools. You save 90% of the boilerplate. For customer-facing APIs, front it with nginx + custom auth + 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';
const userLoader = new DataLoader(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: ids } } });
return ids.map(id => users.find(u => u.id === id));
});
// In resolver:
author: async (post) => userLoader.load(post.authorId)
```
Now 100 posts = 2 queries (all posts + all authors batched).
## 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):
```javascript
import { createComplexityRule } from 'graphql-query-complexity';
plugins: [{
async requestDidStart() {
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema, query: document,
variables: request.variables,
estimators: [simpleEstimator({ defaultComplexity: 1 })],
});
if (complexity > 1000) throw new Error(`Query too complex: ${complexity}`);
},
};
}
}]
```
**Depth limiting:**
```javascript
import depthLimit from 'graphql-depth-limit';
validationRules: [depthLimit(7)]
```
## Common pitfalls
## FAQ
Q
Can I run GraphQL on shared hosting?
Apollo Server (Node.js) — yes on shared cPanel via "Setup Node.js App". Hasura — no, it needs Docker. Use VPS for Hasura.
Q
GraphQL or gRPC for microservices?
gRPC for service-to-service (faster, binary, strict types). GraphQL for client-facing (flexible, introspectable, better DX).
Q
Does GraphQL replace REST completely?
No. Use both — GraphQL for complex client queries, REST for simple webhooks / file uploads / health checks.
Q
How do I version a GraphQL API?
You don't — you add fields and deprecate old ones (@deprecated(reason: "use newField")). GraphQL's schema evolves continuously without breaking clients.
Q
Subscriptions — what VPS size?
4 GB+ for production WebSocket loads. Each connected client uses ~50 KB RAM baseline. 10,000 concurrent = ~500 MB just for connections.