Next.js API routes aren’t just for simple serverless functions; they’re a surprisingly robust way to build a full-fledged GraphQL API that lives right alongside your frontend code.
Imagine you have a Next.js app, and you want to add a GraphQL endpoint. Instead of setting up a separate server, you can leverage Next.js’s built-in API Routes. Let’s say you’re using Apollo Server, a popular choice for building GraphQL APIs.
Here’s a glimpse of what that might look like in pages/api/graphql.ts:
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { readFileSync } from 'fs';
import gql from 'graphql-tag';
import path from 'path';
// Load your GraphQL schema
const typeDefs = gql(readFileSync(path.join(__dirname, '../../schema.graphql'), 'utf-8'));
// Define your resolvers (implementing the logic for your schema)
const resolvers = {
Query: {
hello: () => 'Hello world!',
user: (parent: any, { id }: { id: string }) => ({
id,
name: `User ${id}`,
}),
},
Mutation: {
createUser: (parent: any, { name }: { name: string }) => {
const id = Math.random().toString(36).substring(7);
return { id, name };
},
},
};
// Set up Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Export the handler for Next.js API routes
export default startServerAndCreateNextHandler(server);
And your schema.graphql might look like this:
type Query {
hello: String!
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User
}
type User {
id: ID!
name: String!
}
When a request hits /api/graphql, Next.js invokes startServerAndCreateNextHandler, which translates the incoming HTTP request into something Apollo Server understands, executes the GraphQL operation, and sends the response back.
The core problem this solves is simplifying your deployment and development workflow. You get the benefits of GraphQL—flexible data fetching, strong typing, introspection—without the overhead of managing a separate backend service. Your GraphQL API lives in the same codebase and deploys as part of your Next.js application.
Internally, startServerAndCreateNextHandler acts as a bridge. It takes the raw request and response objects from Next.js, formats them for Apollo Server, and then takes Apollo Server’s computed response and formats it back for Next.js. This allows Apollo Server to run within the serverless environment of Next.js API routes.
The exact levers you control are your typeDefs and resolvers. typeDefs define the shape of your data and the operations (queries, mutations, subscriptions) your API supports. resolvers are the functions that actually fetch or manipulate that data. You can also configure Apollo Server with various options, such as context creation, error handling, and plugin integration, all within your API route file.
A common pattern is to fetch data from an external service or database within your resolvers. For example, to fetch a user from a database:
// Inside your resolvers object
user: async (parent: any, { id }: { id: string }, context: any) => {
// context might contain your database connection or auth info
const user = await context.db.users.findUnique({ where: { id } });
return user;
}
This pattern allows you to keep your GraphQL API logic colocated with your frontend, making it easy to manage and scale.
What most people don’t realize is that the context object passed to your resolvers can be dynamically generated on each request. This is where you’d typically inject things like database connections, authentication tokens, or even other service clients, making your API routes powerful and stateful per request without needing a persistent server process.
The next step you’ll likely encounter is handling more complex data fetching patterns, like N+1 query problems, and optimizing resolver performance.