GraphQL lets clients request exactly the data they need in a single request, avoiding the over-fetching and multiple round-trips common with REST. This guide covers deploying a GraphQL server on your VPS.
Prerequisites
- Node.js installed — see How to Install Node.js on Ubuntu & Debian (with NVM)
- Nginx installed as a reverse proxy
Step 1 — Set Up a Basic GraphQL Server (Apollo Server Example)
mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphql express cors body-parser
Step 2 — Define a Schema
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
Step 3 — Define Resolvers
const resolvers = {
Query: {
users: async () => await db.query('SELECT * FROM users'),
user: async (_, { id }) => await db.query('SELECT * FROM users WHERE id = ?', [id]),
},
Mutation: {
createUser: async (_, { name, email }) => {
const result = await db.query('INSERT INTO users (name, email) VALUES (?, ?)', [name, email]);
return { id: result.insertId, name, email };
},
},
};
Step 4 — Start the Server
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
app.use('/graphql', cors(), bodyParser.json(), expressMiddleware(server));
app.listen(4000, '127.0.0.1', () => console.log('GraphQL server running'));
Step 5 — Manage with PM2
pm2 start index.js --name graphql-api
pm2 startup
pm2 save
Step 6 — Configure Nginx as a Reverse Proxy
server {
listen 80;
server_name graphql.yourdomain.com;
location /graphql {
proxy_pass http://127.0.0.1:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Step 7 — Add HTTPS
sudo certbot --nginx -d graphql.yourdomain.com
Disabling Introspection and Playground in Production
By default, GraphQL servers often expose a schema introspection feature and an interactive query playground — useful in development, but a potential information disclosure risk in production:
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
});
Implementing Query Complexity Limits (Important for GraphQL Security)
Unlike REST, a single GraphQL query can request deeply nested data, potentially causing excessive database load from what looks like one simple request. Use a query complexity analysis library to reject overly expensive queries before execution.
Rate Limiting a GraphQL Endpoint
Since GraphQL typically uses a single endpoint, per-request rate limiting (see How to Rate Limit an API with Nginx) applies at the endpoint level; consider combining with query complexity limits for more granular protection specific to GraphQL's flexible query nature.
Authentication in GraphQL
app.use('/graphql', expressMiddleware(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.split(' ')[1];
const user = token ? verifyToken(token) : null;
return { user };
},
}));
See How to Set Up API Authentication with JWT for the underlying token verification approach.
Common Errors
"Cannot query field X on type Y" — the client's query doesn't match your defined schema; verify field names and types match exactly.
Slow queries — often caused by the N+1 query problem in resolvers fetching related data individually per item; use a batching/caching library (like DataLoader) to resolve this.
Best Practices
- Disable introspection and playground in production
- Implement query complexity limits to prevent resource-exhaustion attacks
- Watch for N+1 query patterns in resolvers, same as any ORM-based application
Related Articles
- How to Choose Between REST, GraphQL, and gRPC
- How to Set Up API Authentication with JWT
- Nginx as a Reverse Proxy for Node.js/Docker Apps
