Securing and Deploying Your GraphQL API
You’ve made it! By now, you’ve learned how to set up, query, and modify data with GraphQL. You’ve even tackled more complex structures with relationships and nested queries. But we’re not quite finished yet—what good is a powerful API if it isn’t secure and deployable? This final tutorial is all about locking down your GraphQL API and getting it ready for the real world.
We’ll cover how to add authentication, implement authorization, set up rate limiting, and finally, deploy your API to the cloud. Plus, I’ll throw in some best practices for keeping everything safe from common vulnerabilities.
By the end of this tutorial, you’ll have a solid, secure GraphQL API that’s ready to handle the demands of production environments, whether for web, mobile, or desktop applications.
1. Adding Authentication
Let’s talk about authentication. If you’ve ever logged into a website or app, you’ve experienced authentication. It’s the process of proving who you are before you can access certain features or data. For your GraphQL API, this means that some queries or mutations should only be accessible to users who are logged in.
We’ll use JWT (JSON Web Tokens) to handle authentication. It’s a popular and simple way to secure APIs, and it works by assigning each user a unique token when they log in.
Step 1: Install JWT Packages
First things first, let’s install the required JWT packages:
npm install jsonwebtoken bcryptjs
- jsonwebtoken: This package helps us create and verify JWT tokens.
- bcryptjs: We’ll use this to hash user passwords before storing them in our database.
Step 2: Create Login Mutation
We need to add a mutation that allows users to log in and receive a JWT token. Here’s how the schema might look:
type Mutation {
login(email: String!, password: String!): AuthPayload
}
type AuthPayload {
token: String
user: User
}
When a user logs in with their email and password, we’ll check if the credentials are correct. If they are, we’ll generate a JWT token and send it back to the user along with their user info.
Step 3: Implement the Login Resolver
Next, we’ll implement the resolver for the login mutation:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const resolvers = {
Mutation: {
login: async (_, { email, password }, { dataSources }) => {
const user = await dataSources.userAPI.findUserByEmail(email);
if (!user) {
throw new Error('User not found');
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new Error('Invalid password');
}
const token = jwt.sign({ userId: user.id }, 'your_secret_key');
return {
token,
user
};
}
}
};
- We first search for the user by email in our data source.
- If the user exists, we check if the password matches the hashed password using bcrypt.
- If everything checks out, we generate a JWT token using jsonwebtoken and return it along with the user info.
Step 4: Securing Resolvers with JWT
Now that we have JWT authentication in place, let’s lock down some of our resolvers. For example, you may want to prevent unauthorized users from accessing certain queries or mutations.
Here’s a modified resolver for fetching user data, which now checks for a valid JWT token:
const jwt = require('jsonwebtoken');
const resolvers = {
Query: {
users: async (_, __, { token }) => {
if (!token) {
throw new Error('Not authenticated');
}
const decodedToken = jwt.verify(token, 'your_secret_key');
return await dataSources.userAPI.getUsers(decodedToken.userId);
}
}
};
- The users query now requires a valid token to access.
- If the token is missing or invalid, we throw an error.
- We decode the JWT token to extract the userId and pass it along to fetch the right data.
You can apply this pattern to other resolvers that need to be protected.
2. Authorization with Roles
Authentication answers who you are, but authorization determines what you can do. For example, a regular user might be able to read data, but an admin can create, update, or delete data.
Let’s add a simple role-based authorization system to our GraphQL API.
Step 1: Add Roles to User Schema
We need to modify our User type to include a role. Here’s an updated User schema:
type User {
id: Int
name: String
email: String
role: String
}
- The role field will store whether a user is an admin or a regular user.
Step 2: Implement Authorization in Resolvers
In our resolvers, we can now check the user’s role before allowing certain actions. For example, only admins should be allowed to delete a user.
const resolvers = {
Mutation: {
deleteUser: async (_, { id }, { token }) => {
if (!token) {
throw new Error('Not authenticated');
}
const decodedToken = jwt.verify(token, 'your_secret_key');
const user = await dataSources.userAPI.findUserById(decodedToken.userId);
if (user.role !== 'admin') {
throw new Error('Not authorized');
}
return await dataSources.userAPI.deleteUser(id);
}
}
};
- We first check if the user is authenticated.
- We then check if the user’s role is admin before allowing them to delete a user.
This ensures that only users with the right permissions can perform certain actions.
3. Rate Limiting
No one wants their API to be overwhelmed with too many requests. That’s where rate limiting comes in. It’s a technique used to limit the number of requests a user or client can make within a certain time frame.
There are many ways to implement rate limiting, but we’ll use the express-rate-limit package to demonstrate how it can be done with GraphQL.
Step 1: Install express-rate-limit
First, let’s install the package:
npm install express-rate-limit
Step 2: Set Up Rate Limiting in Your Server
Now we’ll add rate limiting to our GraphQL server. Here’s how to set it up:
const rateLimit = require('express-rate-limit');
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
const server = new ApolloServer({
typeDefs,
resolvers
});
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`Server running at http://localhost:4000${server.graphqlPath}`)
);
- express-rate-limit limits each IP to 100 requests every 15 minutes.
- You can adjust the windowMs and max values based on your needs.
This helps protect your API from being overwhelmed by too many requests from a single client.
4. Deploying Your API to the Cloud
Now that our API is secure, let’s deploy it to the cloud. There are plenty of options for deploying Node.js apps, but we’ll focus on two popular ones: Heroku and Vercel. Both services offer free tiers and make it easy to deploy Node.js apps with minimal setup.
Option 1: Deploying to Heroku
Heroku is a platform-as-a-service (PaaS) that lets you deploy, run, and manage Node.js apps in the cloud.
Here’s how to deploy your API to Heroku:
-
Install the Heroku CLI:
npm install -g heroku
-
Log in to Heroku:
heroku login
-
Create a new Heroku app:
heroku create
-
Deploy your code:
git push heroku master
-
Open your app:
heroku open
That’s it! Your GraphQL API is now live on Heroku. You can manage your app through the Heroku Dashboard or use the CLI to scale and monitor your app.
Option 2: Deploying to Vercel
Vercel is another great option for deploying Node.js apps, especially if you’re already familiar with it from Next.js projects.
Here’s how to deploy your API to Vercel:
-
Install the Vercel CLI:
npm install -g vercel
-
Log in to Vercel:
vercel login
-
Deploy your app:
vercel
-
Follow the prompts to configure and deploy your app.
Once deployed, Vercel will give you a live URL where your GraphQL API can be accessed.
5. Best Practices for API Security
Before we wrap up, let’s talk about some key security best practices to keep your GraphQL API safe:
- Use HTTPS: Always use HTTPS to encrypt data between your server and clients.
- Validate Inputs: Never trust user inputs. Always validate and sanitize data to prevent attacks like SQL injection or XSS.
- Limit Data Exposure: Only expose the fields and data that are absolutely necessary. Avoid exposing sensitive information like passwords or tokens in your queries.
- Rate Limiting: As we discussed earlier, rate limiting helps prevent DDoS attacks and abuse.
- Error Handling: Be careful with error messages. Don’t expose too much information about your server or database in error responses.
Wrapping Up
Congratulations! You’ve now built a fully secure and deployable GraphQL API. Here’s a quick recap of what we covered:
- JWT authentication for securing your API.
- Authorization with roles to restrict access based on user permissions.
- Rate limiting to prevent abuse and protect your server.
- How to deploy your API to the cloud using Heroku or Vercel.
- Key security best practices for keeping your API safe from common vulnerabilities.
With your new API up and running, it’s ready to serve clients across web, mobile, and desktop platforms. Keep experimenting, building, and tweaking. Your next step? Start integrating this API into real-world apps and watch your ideas come to life!
Happy coding!