Blog

Dependency Injection with NextJS and TypeScript

Coming from a backend world, one thing that stands out when you start writing code in JavaScript, be it React, NextJS, Vue etc, is there's no great focus on code structure. Things like TypeScript bring back the concept of type checking that you'd be used to with a compiled language, and NextJS will give anyone familiar with ASP.NET MVC an alternative pattern for how a website should be constructed. But you can get the whole way through the NextJS tutorial without concepts like single responsibility, inversion of control or CQRS being mentioned once.

Now if your building a small site, maybe you can get away with not knowing or imlementing these things, but if you want to make code that's scalable or maintainable, it doesn't matter if you write in JavaScript or C# the same issues will exist but fortunately the same solutions do too.

Code Smells

Lets take a look at one of the functions on this site which gets the latest blog posts on the homepage.

import gql from 'graphql-tag';
import { Client, ApolClient } from '../prismicHelpers'

// Models
import { FeaturedPost } from "../../Models/FeaturedPost"

const latestPostsQuery = gql`
query latestPosts($category: String) {
allPosts (where : {category: $category}, first : 10, sortBy: post_date_DESC){
  edges {
    node {
      category {
        ... on Categories {
          name
        }          
      },
      title,
      image,
      post_date
      _meta {
        uid
      }
    }
  }
}
}
`;

export const getLatestPosts = async (category?: String) : Promise<FeaturedPost[]> => {
const queryOptions = {
  query: latestPostsQuery,
  variables: { category },
};

return new Promise((resolve, reject) => { ApolClient.query(queryOptions).then(response => {
  var posts: Array<FeaturedPost> = [];
  response.data.allPosts.edges.map((edge: { node: { title: { text: any; }[]; category: any; image: any; post_date: Date; _meta: { uid: any; }; }; }, key: any) => {
    posts.push({
      type: "post",
      title: edge.node.title[0].text,
      image: edge.node.image,
      uid: edge.node._meta.uid,
      category: edge.node.category?.name,
      postDate: edge.node.post_date
    })
  })
  resolve( posts);
}).catch(error => {
  reject(error);
});
});
};

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.

However even though I've seen countless JavaScript examples which don't even do these good things, if this were C# and I was reviewing a pull request, I'd say it smelt and needed changing.

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

There's nothing specifically NextJS about TSyringe, you could use it with any TypeScript/JavaScript project, it just happens that that's what my blog is built using. However it also had a few issues getting it to work and I couldn't any articles around which explained how.

To set it up...

First install the package tsyring and reflect-metadata using npm.

npm install --save tsyringe reflect-metadata

Modify your tsconfig.json file to include the following settings, which will allow the use of decorators in TypeScript.

{
"compilerOptions": {
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}
}

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.

npm install -D @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators babel-plugin-transform-typescript-metadata

Add a .bablerc file to your project with the following settings.

{
"presets": ["next/babel"],
"plugins": [
  "babel-plugin-transform-typescript-metadata",
  ["@babel/plugin-proposal-decorators", { "legacy": true }],
  ["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}

Finally add this import to your _app.tsx file.

import "reflect-metadata";

Using TSyringe

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.

import { DocumentNode } from "graphql";

export interface graphClient {
query(query: DocumentNode, variables?: {}): any;
}

With this interface I can now turn my getLatestPosts function into a class with a constructor that will take in the instance of graphClient.

import { inject, injectable } from "tsyringe";
import { iGetLatestPosts } from "./iGetLatestPosts";
import gql from "graphql-tag";
import { graphClient } from "../iGraphQl";

// Models
import { FeaturedPost } from "../../Models/FeaturedPost";

@injectable()
export class getLatestPosts implements iGetLatestPosts {
graphClient: graphClient;

constructor(@inject("graphClient") private graphClientParam: graphClient) {
  this.graphClient = graphClientParam;
}

private latestPostsQuery = gql`
  query latestPosts($category: String) {
    allPosts(
      where: { category: $category }
      first: 10
      sortBy: post_date_DESC
    ) {
      edges {
        node {
          category {
            ... on Categories {
              name
            }
          }
          title
          image
          post_date
          _meta {
            uid
          }
        }
      }
    }
  }
`;

public getLatestPosts = async (
  category?: String
): Promise<FeaturedPost[]> => {
  return new Promise((resolve, reject) => {
    this.graphClient
      .query(this.latestPostsQuery, { category })
      .then((response: any) => {
        var posts: Array<FeaturedPost> = [];
        response.data.allPosts.edges.map(
          (
            edge: {
              node: {
                title: { text: any }[];
                category: any;
                image: any;
                post_date: Date;
                _meta: { uid: any };
              };
            },
            key: any
          ) => {
            posts.push({
              type: "post",
              title: edge.node.title[0].text,
              image: edge.node.image,
              uid: edge.node._meta.uid,
              category: edge.node.category?.name,
              postDate: edge.node.post_date,
            });
          }
        );
        resolve(posts);
      })
      .catch((error: any) => {
        reject(error);
      });
  });
};
}

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.

import { autoInjectable } from "tsyringe";
import { DocumentNode } from "apollo-link";
import ApolloClient from "apollo-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { graphClient } from "./iGraphQl";

@autoInjectable()
export class apolloGraphClient implements graphClient {
apolloClient: ApolloClient<NormalizedCacheObject>;

constructor(apolloClient: ApolloClient<NormalizedCacheObject>) {
  this.apolloClient = apolloClient;
}

public query = async (query: DocumentNode, variables?: {}): Promise<any> => {
  const queryOptions = {
    query: query,
    variables: variables,
  };

  return new Promise((resolve, reject) => {
    this.apolloClient
      .query(queryOptions)
      .then((response: any) => {
        resolve(response);
      })
      .catch((error: any) => {
        reject(error);
      });
  });
};
}

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.

import Head from "next/head";

import React, { useState } from "react";
import { container } from "tsyringe";
import { GetStaticProps } from "next";
import Layout from "../layouts/layout";
import { FeaturedRow1, FeaturedRow1Model } from "../components/featured-row-1";
import SectionHeading from "../components/section-heading";
import { iGetLatestPosts } from "../utils/queries/iGetLatestPosts";

export default function Home({
latestPosts,
webDevelopmentPosts,
sitecorePosts,
devOpsPosts,
}: {
latestPosts: FeaturedRow1Model;
webDevelopmentPosts: FeaturedRow1Model;
sitecorePosts: FeaturedRow1Model;
devOpsPosts: FeaturedRow1Model;
}) {
return (
  <Layout>
    <Head>
      <title>Hi My Name Is Tim</title>
    </Head>
    <SectionHeading heading="Latest Posts" link="blog"></SectionHeading>
    <FeaturedRow1 posts={latestPosts}></FeaturedRow1>
    <SectionHeading heading="Web Development" link="web-development"></SectionHeading>
    <FeaturedRow1 posts={webDevelopmentPosts}></FeaturedRow1>
    <SectionHeading heading="Sitecore" link="sitecore"></SectionHeading>
    <FeaturedRow1 posts={sitecorePosts}></FeaturedRow1>
    <SectionHeading heading="Devops" link="devops"></SectionHeading>
    <FeaturedRow1 posts={devOpsPosts}></FeaturedRow1>
  </Layout>
);
}

export const getStaticProps: GetStaticProps = async () => {
// Resolve interface for iGetLatestPosts
const instance = container.resolve<iGetLatestPosts>("iGetLatestPosts");

const latestPosts = await instance.getLatestPosts();
const webDevelopmentPosts = await instance.getLatestPosts("X8kFhxIAACcAn9oY");
const devOpsPosts = await instance.getLatestPosts("X8kFlRIAACkAn9pa");
const sitecorePosts = await instance.getLatestPosts("X8kFeBIAACkAn9nV");

return {
  props: {
    latestPosts: latestPosts,
    devOpsPosts: devOpsPosts,
    sitecorePosts: sitecorePosts,
    webDevelopmentPosts: webDevelopmentPosts,
  },
};
};

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.

container.registerInstance(
ApolloClient,
new ApolloClient({
  link: PrismicLink({
    uri: prismicGraphUri,
    repositoryName: prismicRepoName,
  }),
  cache: new InMemoryCache({ fragmentMatcher }),
})
);

container.register(&quot;graphClient&quot;, apolloGraphClient);
container.register(&quot;iGetLatestPosts&quot;, getLatestPosts);

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.

Summary

We have seen that using JavaScript doesn't prevent us from using the same concepts as we would with any other more traditional backend language.

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.

Automated Deployments with Sitecore Managed Cloud

I think it's fair to say automated deployments are a fairly standard practice at this point, long gone are the days when we would remote onto a server and copy some files over.

However when you get your shiny new Sitecore managed cloud environment, unlike other cloud offerings it doesn't come with any automated deployments setup for you. Which to be frank is a bit of a let down. Getting one setup though isn't the hardest thing in the world.

A few requirements we're after:

  1. Deployments should be a button click, no copying and pasting files.
  2. Deployments should get promoted through multiple environments. e.g. Test, Staging and Production.
  3. Deployments should be gated and approved to be released.
  4. There shouldn't be any downtime during the deployment.

So lets dive in.

Azure Devops

The first thing to note, is that Sitecore Managed Cloud is essentially vanilla Microsoft Azure with Sitecore setup on it. As a developer or devops engineer you get given a regular login to the Azure portal and as far as I've seen there's nothing specifically Sitecore about it.

This means however you would normally deploy other things to Azure will work perfectly well for Sitecore. My preference is to use Azure Devops as being from Microsoft it fits really well into the Azure ecosystem.

To do this, first off sign up for Azure Devops, get a pipeline created that compiles your solution and outputs it to an artifact. I'm going to skip past all the details on how you do this as it's not specific to managed cloud and at this point lots of people have written articles on how to do it, there's also a high chance your solution is already doing this. If not then go Google it :)

Release Pipeline

To do a release we need a release pipeline. Unlike build pipelines which control a build and are written in yaml, release pipelines control a release and are created through a UI. With these you can configure the different steps, add gates, configure variables, everything you need for a release.

Once you've got a Devops project and your solution compiling, the next step is to create a release pipeline. So head onto the release pipeline section and click the big blue button to make your first pipeline.

Sitecore Managed Cloud runs Sitecore as Azure App Services, so you will want to pick the Azure App Service deployment template.

This template will create a stage with a job, but before it can release anything it needs an artifact to deploy.

Click the Add an Artifact button and pick the artifact from your build. On the artifact you can now click a lightening bolt icon to set up continuous deployment triggers. From here you can set the release to run whenever there is a new build on your master / main branch.

The deployment stage will also need some details completing so click into that next.

Now when you try to pick the Azure subscription for Sitecore Managed Cloud, you will likely get an error saying Failed to create an app in Azure Active Directory. Error: Insufficient privileges to complete the operation. Ensure that the user has permissions to create an Azure Active Directory Application.

Don't worry, you can't solve this on your own, but Sitecore Support can fix it for you. To get Azure Devops connected to the azure subscription you need a service principal setup. They will know what you mean and send you a service principal id and key.

To setup the service principal go to, Project Settings > Service Connections > New Service Connection.

Pick Azure Resource Manager.

And Service Principal (manual).

Enter the details and you'll have a connection into Sitecore Managed Cloud.

Back in the release pipeline you can now select the Azure subscription and App service name. Run the pipeline and your solution will deploy.

You now have a solution deploying to the Azure App Service. Some things to note:

1. This process does not delete any files already on the target machine. This is intentional as with managed cloud the files have a split responsibility. Sitecore installs Sitecore and manage the SSLs which include config file changes, none of which will be in your source control. You however are doing everything else.

2. Right now whenever you deploy the site will experience downtime as there no concept of green / blue deployments setup.

Blue / Green Deployments

One of the great things about using Azure App Services is all the infrastructure stuff like load balancers, multiple instances etc is done for you. If you want to scale horizontally and add instances, you just move a slider and all the deployments and load balancing is done for you.

If you want to do a blue / green deployment that can be done for you using slots.

Creating an App Service Deployment Slot

To create a slot navigate to the app service you are deploying to, and select Deployment Slots on the left nav.

Click Add Slot, give it a name. e.g. staging and choose to clone the settings from the existing default slot.

You will now have a second slot that can be deployed to and then swapped to be the live one. When slots are swapped the application is warmed up first meaning no downtime when the slot happens.

However although you copied the settings from the original slot, your staging slot won't have an application on it yet and as your source control won't contain the whole of Sitecore, you will need to copy it from the existing slot.

Connecting to Deployment Slots with FTP

The easiest way to setup the slot with the base files is to copy them with FTP. To get the FTP details select one of the slots from the slots list and then select Deployment Center in the left nav.

Now in the top nav, select Deployment Credentials.

A panel will now show giving you the FTP details.

Repeat this with the other slot and you now have the details to be able to FTP to the different environments and copy the contents from one to the other.

Configuring Azure Pipelines to deploy to a slot

The last thing on our list is to start deploying to the slot and then swapping the slot to achieve no downtime.

Within Azure Devops navigate back to the release pipeline and edit the pipeline. Go into the job/task and select the Deploy Azure App Service task. Under the App Service Name setting you will see a checkbox to deploy to a slot. Select this and then complete the details for Resource Group and Slot.

If you were to deploy now, your code would be deployed to the slot rather than the production instance. You could then preview the site running on the slot and manually swap the slots in the Deployment slots section of the portal.

Alternatively you can configure the release pipeline to swap the slots for you.

Add a new task to the pipeline job called Azure App Service Manage. This will come up if you search for swap.

Complete the details for the task, setting Action to Swap Slots, and enter your App Service name, Resource group, and the Source Slot that it needs to swap with. Check the checkbox to Swap with Production.

With this all configured you can now run your pipeline and have an automated deployment with no downtime. The solution will first be deployed to the slot and then the slot swap action will swap the two over. When it does this the staging slot will be warmed up first meaning that when the swap completes there will be no downtime as Sitecore will have already started.

Exporting a Database from SQL Azure

If you need to copy your SQL Azure database locally then exporting it as a Bacpac file is the simplest route to go.

There's many ways you can do this including PowerShell, REST API, and even SQL Server Management Studio. However I am going to show you how to do it through the Azure Portal.

Exporting a Bacpac file

First off find your DB in the Azure Portal.

In the top set of buttons when you view your DB (make sure you're on the actual DB not the server), you will see an export button 3rd from the left.

Clicking Export takes you to a page where you need to configures the filename to export as, the storage account to export to and the authentication to log into the DB. The storage account is required as a place for the export to be saved, you will get the choice of configuring the container for it to go into, so I like to create one called backups.

Checking the status of the export

When you start the export you will get a confirmation that it has started, but knowing how to check what the progress is isn't obvious.

To check the progress, navigate to the DB server in the portal.

In the left nav, under Data management, select Import/Export history.

From here you can view the status of each of the exports.

Once they are complete you can download them through blob storage.