Welcome to FullStack. We use cookies to enable better features on our website. Cookies help us tailor content to your interests and locations and provide other benefits on the site. For more information, please see our Cookies Policy and Privacy Policy.
Creating an Express and GraphQL Server from Scratch
Written by
Carlos Angarita
Last updated on:
October 1, 2025
Written by
Last updated on:
October 1, 2025
GraphQL is a language designed for APIs that only loads the specific data each client requests, which is its main attraction. Initially created by Facebook, its growing ecosystem formed the GraphQL Foundation, a neutral home to the GraphQL community. As the title suggests, we are going to be implementing a GraphQL server in Node using Express to create an API for a shopping list. So, let's create our project!
mkdir GraphQLShopping
cd GraphQLShoppingnpm init -ynpm install express express-graphql graphql
We’ve just created a new Node project and installed Express and GraphQL. Next, we need to create index.js, the entry point for our app and we need a route to call from our clients. The great thing about GraphQL is that you only need one route instead of the multitude of routes that we usually have in a REST API. This route receives a query with the structure of the data and returns a JSON object with the data in the requested structure. For this tutorial, we are going to create the /graphql route:
We created the route and used the express-graphql middleware with a couple of options: an empty schema and the graphiql flag set to true. Activating graphiql allows us to have a nice tool within the browser to talk with the GraphQL server so when we browse http://localhost:3000/graphql we get:
I would like to call your attention to the error message displayed:
{
"errors": [
{
"message": "Query root type must be provided." }
]
}
This message shows up because of the empty schema we created earlier. So, how does it work? Schemas in GraphQL are the way we define our queries, meaning what types and relations between types can GraphQL resolve. Types are objects that define the structure of the data using the GraphQL schema language, a language-agnostic way to specify the shape of our data.
What did we do? We created a GraphQLObjectType, gave it a name, and defined a set of fields. Each field has a type from the GraphQL type system. The id is a GraphQLID, which accepts both strings and numbers, and the name is a GraphQLString that defines the field as a string. You can see the rest of the types in the GraphQL documentation.
Schema definition
Let’s create the schema.js file. Now that we can create types, it is time to define a query for the data of that type in our schema. Let’s first import GraphQL:
Note that we’ve not used the userId property for the item, since we’ll be using it later when defining the relationships. Now that we have our types, it is time to define our queries and schema.
We defined a query for our items inside the RootQuery object, the type is an instance of GraphQLList which, as the name suggests, tells GraphQL that it is a list of the shape of ItemType, which we previously defined. Something interesting to note is the resolve function: if we query for items, the resolve function should return a list of items that match the ItemType shape. Finally, we export an instance of the GraphQLSchema with our query. This is the schema that we need to provide to the express-graphql middleware.
Great! There is no error message and in the panel on the right we can see our schema definition. Now it's time to make our queries.
Queries and relationships
A GraphQL query is a specially structured string that we defined to obtain some data from the server. Let's see how it's structured.
{
items{
id
text
completed
}
}
So we put the items string inside curly brackets; this string must match the items key we put inside fields on the RootQuery object. Then, we put all the fields from the ItemType object that we want to retrieve inside curly brackets; in this case: id, text and completed. When we press play we get the following:
Awesome, we have our item list! What if I want to know which user created the item – how can we create a relationship? Let's get back to the ItemType and do that.
We've added the user property to the ItemType and said that it’s the UserType we previously defined. Now if a client wants to load the relationship, GraphQL can get the data from the resolve function. If we need to load the user data, it will find a user whose id matches the parent userId value. That parent is an item from the shopping list, so GraphQL gives us access to all of its properties and calls the resolve function for every item on that list. To query the items and the name of the user that created it, we use the following:
{
items{
id
text
completed
user{
name
}
}
}
And we get:
Great! We have the data we were looking for! But wait, what if we want to get all the items from the shopping list of a specific user? We will need to define some arguments and filter the result.
Now GraphQL knows about some arguments that are needed when querying the items, named the userId args. We told GraphQL that this argument is of GraphQLID type, which means we can pass a string or a number as a value. Then, we modified the resolve function to use the args parameter to filter the shopping items. The args parameter is an object with the userId data that we previously defined. So this translates to a query like:
{
items(userId:1){
id
text
completed
user{
name
}
}
}
We use parentheses to pass some arguments to the items query, in this case, the previously mentioned userId. Now if we run the query it should show something like:
And we get only the items from the user with id equal to 1. But, in the same way, we can define more args and filter them as needed.
Mutations
Now that we have a way for the client to query the shopping list and the user, let’s find a way to update that list. Let’s use mutations to add a new item: a mutation is a convention to signal a possible data-write from a query. We are going to create a new GraphQLObjectType for our mutation type; we set the args as the fields we want to save and the resolve function will handle the saving.
Then we add the new mutations object to the schema and then query:
module.exports = new GraphQLSchema({
query: RootQuery,
+ mutation: Mutation
});
Notice that we had to add the mutation keyword before the curly brackets in order to signal GraphQL that what we want to invoke may be written to disk!
mutation {
addItem( id: "ee4e73ea-62d4-11ea-bc55-0242ac130003",
text: "Aples",
qty: 12,
completed: false,
userId: 2) {
id
text
qty
user {
name
}
}
}
Now we have a way to query and update our data using GraphQL. As we can see, the mutation returned the created shopping item. We can do the same as with any other GraphQL type and request only certain fields or load the related user.
Conclusion
Thank you for reading this article. I hope you have gotten a taste of the power of GraphQL. This layer is completely agnostic to your data storage – you could have a file, an SQL database, a NoSQL database, or even an in-memory object like in this example – you just need to replace the resolve function on the GraphQLObjectType, which can also be async. GraphQL is useful for a variety of reasons, but especially because it loads precisely the data your client needs, which leads to decreases in network requests and smaller payloads. These features are especially attractive to mobile clients, where size and number of requests can have a significant impact on user experiences.
Using techniques like what is listed above, we have had the opportunity to address our clients’ concerns and they love it! If you are interested in joining our team, please visit our Careers page.
--- At FullStack Labs, we are consistently asked for ways to speed up time-to-market and improve project maintainability. We pride ourselves on our ability to push the capabilities of these cutting-edge libraries. Interested in learning more about speeding up development time on your next form project, or improving an existing codebase with forms? Contact us.
GraphQL is a query language for APIs that allows clients to request exactly the data they need — nothing more, nothing less. Unlike REST, where multiple endpoints often return redundant data, GraphQL uses a single endpoint to serve customized responses, reducing payload sizes and improving performance.
How does Express work with GraphQL in this setup?
Express serves as the HTTP server, and the express-graphql middleware connects your GraphQL schema to the /graphql route. This route handles all incoming queries, executes them against the schema, and returns data in the exact structure the client requests.
How are schemas and types defined in GraphQL?
In this tutorial, the schema defines two main types: User and Item. Each type specifies the shape of the data, such as fields for id, name, text, quantity, and completed status. The RootQuery object within the schema determines how data can be fetched, while resolver functions handle retrieving and returning that data.
How does GraphQL handle relationships between data?
GraphQL allows you to define relationships directly within the schema. For example, each item in the shopping list is linked to a user through the userId field. By adding a user field in the Item type and writing a resolver, clients can request user data alongside item data in a single query, reducing the need for multiple requests.
What are GraphQL mutations, and how are they used here?
Mutations allow you to create, update, or delete data in GraphQL. In this project, the addItem mutation lets clients add a new shopping list item by passing fields like id, text, quantity, completed status, and userId. The resolver saves the new item and returns the requested fields, which helps keep responses lightweight and flexible.
AI is changing software development.
The Engineer's AI-Enabled Development Handbook is your guide to incorporating AI into development processes for smoother, faster, and smarter development.
Enjoyed the article? Get new content delivered to your inbox.
Subscribe below and stay updated with the latest developer guides and industry insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.