I spent a good amount of time through articles and peaking into open source projects. I could never really find any be all tips and tricks for GraphQL structure of directories and files and separating schemas apart. Most tutorials were written with everything in one primary file.
In this article I go over how I've started to structure my GraphQL server applications.
Before I begin, please note that I'll be writing this with the hope you have at least a basic understanding of GraphQL, as well as Apollo-Server when it comes to setting up an Apollo Server, creating schema type definitions as well as resolver functions and data sources.
So let's chat about all these Apollo-Server tutorials out there. They're awesome! We're still very early on in GraphQL's adoption in our everyday stacks even though it appears to be the opposite from the amount of content that has been put out regarding it's positive impacts on data fetching. As I started to learn about GraphQL these tutorials were a great resource. I watched stuff on Frontend Masters, courses on Egghead, and read plenty of articles on Medium.
The one thing I couldn't really wrap my head around was how everyone was really organizing their files for types and resolvers regarding the different parts of a more complex application because of how simple the content was. Most of the time it was all kept in one big file and was used directly inline to create the apollo-server instance from only showing 4–5 type definitions and 2–3 resolvers.
I started putting the pieces together from reading multiple places into what I feel like is a good place to start thinking about how to organize your type definitions and your resolvers in a way that makes sense to the resources your consuming from wrapping a REST API or exposing content to a client.
The repository I'll be using is for a small side project I've been working on that wraps the LastFM API endpoints (all of the un-authenticated endpoints) to grab information on my music listening from Spotify for a React application (well technically, anywhere I want it). But this article is focused on the server side organization
We'll start off the with the base Node index.ts file (yes, typescript, if you're not worried about types then just disregard any of that weird looking stuff)
Pretty basic so far, we're simply importing Apollo-Server, dotenv to read the LastFM API Key, the schema which is kept at ./schema and creating the Apollo Server and kicking off the GraphQL server. Next up is taking a look at the ./schema directory.
We have the main index.ts for the entire schema definition along with a directory for resolvers and a directory for types which are broken down into sub directories. One directory for all the shared types/resources. Then a directory for each type of top level resource that we'll be exposing, in this case the LastFM API.
Let's take a deeper look into the ./schema/index.ts
to see what it's importing and exporting which is being passed tonew ApolloServer({ schema }).
This is where we start to separate things out. If you notice we have a RootDefs declaration that creates a graphql document node which has 1 single type Query and what's different about this type definition is it's completely empty. We're going to be extending this root Query type in other files but as of right now the version I have is 14.5.4 of graphql package doesn't allow you to create a type with no fields. So we create a placeholder that does absolutely nothing. We name it _empty and set it's type to String and make sure it's optional (pst, cause it'll never be used)
Then at the bottom of the file we create an array of Document Nodes which is the type created from using the gql tag when writing your type definitions. We then use the spread operator to spread the rest of the imported type definitions which in this case is LastFMSchemaDefsand SharedSchemaDefs onto that array and export from the file.
The hard part is done, let's look at LastFMSchemaDefs and SharedSchemaDefs to see how we extend the root Query type with the rest of our graphql servers types.
So looking at these two files we can see SharedDefs
is very straight forward and creates a basic type that can be used anywhere, we aren't extending the root Query object just yet, and we export is as an array of 1 DocumentNode.
Looking at the second lastfm index.ts
file we have a few changes. First thing you'll notice is we're importing a bunch more type defs at the top, we are importing these into 1 place and exporting as the whole type definition of lastfm/index.ts
to keep things tidy with our imports. From the type definition as the main type def for our LastFM resource we extend type Query with a field of lastfm which has a type of LastFM which we define below which is defined exactly like our Query type was defined at the root def. The imports above all extend this LastFM type in their own specific file which exports a single named export representing the resource name, below is an example (I won't post them all for the sake of time and space).
Each of the lastfm resources have their own directory with a single named export which extends the LastFM type and imported as type definitions in the index file for lastfm/index.ts
file.
Next up is, Resolvers. Resolvers live in a directory under schema named ./schema/resolvers
with a single index.ts
that serves as the base for all imports of resource resolvers, similar to type definitions. Let's take a look at what that looks like.
So similar to the type definitions, at the top of the file we're importing the base import for the LastFMResolvers
which internally imports all resolvers for the rest of our type definitions, as well as SharedResolvers
which we know currently only has a resolver for the type Image
If we look at the root Query resolver, we're setting lastfm as anon function that returns an empty object, but why? Well you can think of this top level lastfm
type as a kind of namespace for all of our nested types that we can query for data depending on the type of resource we're wanting to grab data from lastfm api. *For my typescript peeps, all resolvers which is an object of type IResolvers
which can be imported from graphql-tools
package from npm.
At the bottom of the file we're using lodash.merge
to merge all of the imported resolver objects which are imported above and exporting as 1 single resolvers object that is passed into our apollo-server
config object.
Let's look at LastFMResolvers
to see the final bits of how this is all tied together.
Once again, similar to our type defs for the lastfm resources, we import all of our resolvers for each individual resource from their respective directories, create the LastFM
type resolver which has the nested resources which is where we pass our arguments to and do some basic checks upfront and throw UserInputErrors
which is because of the dynamically required arguments needed. If you know of a better way of handling dynamically changing required arguments, please let me know. In each nested resource we return an options object which will be passed to the resource resolvers and used in our LastFM API calls. Again for brevity, I'll only show the UserResolvers
.
At this point, it's basic graphql resolvers. Setting our type resolvers, grabbing the params from the correct (root, parent, _, or whatever you call the first param of a resolver) and from args and using our defined dataSource to make the call and returning the correct data from the response.
Lastly, is the datasource you see us calling. If you're unfamiliar with Apollo-Server datasources, check out the docs for a pretty quick and easy read. Awesome class that handles most of your needs out of the box for dealing with REST API calls. Apollo Datasources Documentation
Like everything else we've seen today, I keep this at a top level datasources directory next to schema and it has subdirectories for each top level resource type, so for this project a single lastfm directory with a single index.ts
file. Let's take a gander.
Not really much to say about this file, pretty straight forward. Extending the RESTDataSource
that does some checks for a lastfm api key and sets each requests param to json format and throws an AuthenticationError if now api_key is provided and a single call method that setups the query params based on the query arguments and fetches the data.
I really hope this helps anyone that is struggling to find ways of organizing graphql projects. Like most other things out there, most of this is architectural decisions that just make sense to myself and how I think of resources in an apollo-server application. You can easily keep resolvers and types in a single top level directory under ./schema
and have a lastfm directory where you keep resolvers and types together in the same file and exported separately and follow the same conventions above of importing and spreading type defs and merging resolver objects from that file.
To finish this off here's an image of the projects folder structure that I went through.
There will definitely be those that don't like the default named index.ts files in each directory. Which is totally understandable. For much larger projects where I'm working on it with multiple devs I would definitely be naming those appropriately, but when I'm working alone I like to keep my import lines shorter :)
If this helps at all, please comment and let me know - I'm going to start trying to write more technical articles as I continue to work on side projects. One coming down the pipe shortly will be a ReactNative application where I'll discuss everything from concept, to design to development and deployment using expo-cli and expo-kit for native components.
Feel free to catch me on twitter @imjakechapman
Top comments (0)