Build a production GraphQL API with Node.js and Apollo Server — schema design, resolvers, authentication, and N+1 query optimization.

Abdur Razzak
Full-Stack Web Developer
GraphQL lets clients request exactly the data they need — no more, no less. Unlike REST where the server defines the response shape, GraphQL clients specify their own queries. This eliminates over-fetching (getting unused fields) and under-fetching (needing multiple requests for related data). For complex applications with many interconnected data types, GraphQL's flexibility is a significant advantage over REST.
Install @apollo/server and graphql. Create an ApolloServer instance with your typeDefs (schema) and resolvers. In Node.js with Express, use expressMiddleware from @apollo/server/express4 to mount Apollo as an Express middleware. This gives you all of Express's middleware ecosystem (auth, logging, rate limiting) while using Apollo for GraphQL execution.
Your schema defines the types, queries, and mutations. Use the Schema Definition Language (SDL): define types with their fields, Query type for read operations, and Mutation type for writes. Relationships between types are expressed directly in the schema — a User type can have a posts field of type [Post!]!. Design your schema around what your clients need, not around your database structure.
Resolvers are functions that return the data for each field in your schema. The resolver for Query.posts fetches all posts from your database. The resolver for Post.author receives the parent Post object and fetches the related User. Context is passed to every resolver — put your authenticated user, database connection, and data loaders in context.
The N+1 problem occurs when fetching a list of N posts and then making N separate database queries for each post's author. DataLoader solves this by batching individual load calls into a single batch request. Instead of N queries, DataLoader makes one query for all requested user IDs. This optimization is critical for production GraphQL performance — without it, a simple query can trigger hundreds of database calls.
Verify JWTs in Apollo's context function, which runs before every resolver. Extract the token from the Authorization header, verify it, and attach the decoded user to context. In resolvers that require authentication, check if context.user exists and throw an AuthenticationError if not. For field-level authorization, check permissions within the specific resolver rather than at the schema level.