Lets take a look at one of the functions on this site which gets the latest blog posts on the homepage.
This bit of code queries Prismic's GraphQL API to get the latest 10 articles for a category and then maps the result onto an internal FeaturedPost model.
Some good things about it:
- The Prismic logic is abstracted away from the rest of the application by mapping the results to a model.
- It follows single responsibility by doing just 1 job.
- All config for the API (e.g. URI) are separated into a common function rather than being duplicated.
Here's some bad things about it:
- It's taking a dependency on Apollo Client, which while is a great client for doing GraphQL queries, at the rate JS frameworks come and go we can't say we'll never replace it and in a large application that has countless queries that would be a lot of code to update.
- There's also no way anything can call this without also taking a dependency on it. That means I now have a hierarchy of at least 3 functions with each one dependent on the next. If I ever wanted to add unit tests to my project I'd have a big problem.
Dependency Injection using TSyringe
These issues can be solved using Dependency Injection. There's quite a few around but the one I've chosen is TSyringe (https://github.com/microsoft/tsyringe).
It's built by Microsoft, which gives me some confidence in the amount of QA that will have gone into it, but more importantly it works the way I expect a DI framework to work. There's a good chance this could be because I'm used to working in the Microsoft stack and it therefore has natural similarities to DI frameworks in their own languages.
How to add TSyringe to a NextJS project
To set it up...
First install the package tsyring and reflect-metadata using npm.
Modify your tsconfig.json file to include the following settings, which will allow the use of decorators in TypeScript.
Now there's a few more packages your going to need. As of NextJS 12, Babel is no longer used and has been replaced with SWC to provide faster compile times. However TSyringe and most other DI frameworks use decorators to function (hence the tsconfig.js setting to turn them on), but the setting for SWC to allow this set to false in NextJS and there's no way for you to provide your own config. Fortunately Babel is still supported and you can customise it. Hopefully a future version of NextJS will address this.
Install the following packages as dependencies.
Add a .bablerc file to your project with the following settings.
Finally add this import to your _app.tsx file.
We're now ready to convert the code I had before into something more maintainable.
First I'm going to create a GraphQL Client interface which all my queries will use when they want to call a graph API. This has one function called query, which my functions will pass the graph syntax too along with a variables object.
With this interface I can now turn my getLatestPosts function into a class with a constructor that will take in the instance of graphClient.
Some things to note in this new class.
- It's also now implementing an interface so that it can instantiated using DI.
- The @injectable decorator allows TSyringe to inject the dependencies at runtime.
- The constructor is decorating a parameter with @inject("graphClient") which means that parameter will be injected at runtime with whatever is configured against the graphClient token.
- There are imports from tsyringe.
- There are no references to the implementation of graphClient.
- My function now has zero dependencies on Apollo Client and doesn't even know it's being used.
My implementation of graphClient looks like this.
Essentially all this function does is pass the parameters to the query function to an Apollo Client's query function. The Apollo Client itself is also being injected!
You may have expected this file to also instantiate the Apollo Client, and it could have, but I've gone to the extreme and the single purpose of this file is to act as a bridge between the business logic queries and what client is being used, so for that reason its injected.
You'll also notice that this time I'm decorating the class with @autoInjectable() and there is no decorator on the constructor parameter. More on this in a bit.
The homepage page for this site now looks like this.
Pages in NextJS TypeScript don't use classes, so we can't do constructor injection to get the instance of our getLatestPosts query class. Instead we are using container.resolve<iGetLatestPosts>("iGetLatestPosts") to get the instance to token name iGetLatestPosts from the DI container.
Lastly in the _app.tsx file I am registering the classes on the container. I'm only including the relevant bit of the file here.
For the Apollo Client I am using register instance to register a specific instance and creating it at the same time. Notice the first parameter is the class name.
For the graph client and getLatestPosts query I am using the register method and rather than creating the instance of my implementation, just passing the class as the second parameter. The framework will handle creating an instance of them for me.
Notice the first parameter for the second two are strings rather than the actual interfaces. These are token names that the container will use to reference the instance value. With the Apollo Client, the framework will figure out the token name when it adds it to the container, but it can't do the same for an interface (if you try you will get an interface cannot be used as a type error) so you have to provide the token name instead. This is also the reason why the graph client implementation didn't need to use a string to inject the class instance in the constructor, whereas the other places did.
Personally I feel this is a weakness in the framework as it also means there is no type checking either when registering or resolving from the container.
We've also seen how a NextJS application while slightly more awkward, can still be set up to use dependency injections.
Finally we've had a look at how to actually configure some code to have complete separation between logic within a solution.