Development template for scalable SPA’s

Tim Ververs
ihomer academy
Published in
8 min readApr 21, 2023

--

Starting a new project can often feel like reinventing the wheel, even when the underlying architecture is similar to past projects. To save time and streamline our development process, I’ve compiled the best practices and smart ideas from my colleagues into a development template that we can reuse for future projects. This blogpost is scoped to a monorepo created with NX and a Vue SPA deployed on Azure Static Web Apps (SWA) with Azure Functions written in C#. We also discuss authentication with the integrated Azure authentication features in SWA.

Monorepo with Vue.js

The first thing we do is create a monorepo with NX. We go to https://nx.dev/ and create a new integrated repo, and start generating a new Vue app. But wait a minute… no Vue package? Okay, so this is the part where we abort the monorepo setup and go to https://www.npmjs.com/package/@nx-plus/vite (bear with me 🐻) and follow the steps by running (use version 15.7.2 for this to work):

npx create-nx-workspace@^15.7.2

We choose Integrated Repo because we won’t be publishing libs or apps as packages outside the monorepo. We choose ‘apps’ because we install the plugins later (also, this creates the apps/libs structure). We give it a fancy name and enable distributed cache (because it’s free, yo!).

CLI — Creating Workspace in version 15.7.2

You can use the URL in the CLI to connect your workspace to an organization in https://cloud.nx.app/orgs.

The URL in the CLI after completion

Now install the dependencies for testing and linting (also use version 15.7.2)

npm install @nrwl/cypress@^15.7.2 @nrwl/jest@^15.7.2 @nrwl/linter@^15.7.2 --save-dev

We can now finally install the nx-plus vite plugin

npm install @nx-plus/vite --save-dev

Now we can generate our first app (our appname is blog, you can change this)

npx nx g @nx-plus/vite:app blog

You may have noticed the @nx-plus/vite plugin doesn’t have generators for libraries and components, that’s where @nx-plus/vue comes in. Lets install that as well

npm install @nx-plus/vue --save-dev

And generate a library for ui components

npx nx generate @nx-plus/vue:library components --vueVersion=3 --directory=ui --no-interactive

There is a small change we need to apply to the vite.config.js because it uses an old appRootPath from the @nrwl packages and not from the @nx-plus/vite package.

Change

import { appRootPath } from ‘@nrwl/tao/src/utils/app-root’;

To

import { appRootPath } from ‘@nx-plus/vite/src/app-root’;

Let’s test it!

npx nx run blog:serve

This should start your Vue app on http://localhost:5174/. Also the structure in your folders should be something like this.

Seperate apps and libs folder

I’m using VS Code as my IDE, also the NX plugin provides with handy generator tools to create libraries and components inside libraries.

Using the component library and building the app

To use the UI component library in the app I first deleted the HelloWorld component inside the Vue App blog and start referencing my HelloWorld component inside the UI component library using a simpel import

Import in the script tag

You may have noticed I also removed the whole defineComponent inside the App.vue, this is because of the newly added composition API (https://vuejs.org/api/composition-api-setup.html). We can replace this entire block by just typing `setup` in the script tag now. We also do this for the HelloWorld Component inside the UI component library and return some text

Simpel component returning text

The application should now provide you with the changes

I’m also adding a script inside the packages.json so I can use this script more easily inside my build agent later while deploying my application. I’m telling the CLI that npm run build now executes

npx nx run blog:build

which will build my application and its dependencies (ui component lib) into a single dist folder.

Build script in package.json

Azure Functions

Given that our apps needs to communicate to some bussinesslogic I’m creating a serverless Azure Function, in VS Code I press F1 to show all commands and select

Azure Static Web Apps: Create HTTP Function…

Choose your preferred language (I choose C#) and give the function a name and namespace. I choose anonymous authentication for easy testing. This will create an API folder inside your monorepo, you can choose to let it be this way, but I liked it better to move it to the Apps folder under the name blog-api.

This API now returns a simple string saying if it works and allows a query string parameter name.

Auto generated HTTP Trigger Function

Deploying to Azure Static Web App (SWA)

For simplicity, we are now going to create a SWA in the Azure Portal. You could also do this in VS Code using the Azure Plugin, or by using ARM templates or Terraform to deploy consistently. Give the SWA a resource group and name, and select a hosting plan. I’m using Standard because I need this for custom authentication later, but you could use free. Select a region where your functions should be running. Even though I’m using Azure DevOps as my SC, I selected ‘Other’ in the deployment details so I can write the pipeline myself.

Creating the SWA in Azure

Deploy the SWA and after completion go to ‘Manage deployment token’. We can use this token to deploy to this SWA. Copy and save the token for now. Go to Azure Devops to create a pipeline for deploying the application, create a new pipeline with the following .yml code

trigger:
- master
pr:
- master

variables:
CI: 'true'
${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
NX_BRANCH: $(System.PullRequest.PullRequestNumber)
TARGET_BRANCH: $[replace(variables['System.PullRequest.TargetBranch'],'refs/heads/','origin/')]
BASE_SHA: $(git merge-base $(TARGET_BRANCH) HEAD)
${{ if ne(variables['Build.Reason'], 'PullRequest') }}:
NX_BRANCH: $(Build.SourceBranchName)
BASE_SHA: $(git rev-parse HEAD~1)
HEAD_SHA: $(git rev-parse HEAD)

jobs:
- job: master
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
fetchDepth: 0

- script: npm ci
- script: npx nx affected --base=$(BASE_SHA) -t lint --parallel=3
- script: npx nx affected --base=$(BASE_SHA) -t test --parallel=3 --configuration=ci
- script: npx nx affected --base=$(BASE_SHA) -t build --parallel=3

- task: AzureStaticWebApp@0
inputs:
app_build_command: 'npm run build'
output_location: 'dist/apps/blog'
api_location: 'apps/blog-api'
skip_app_build: false
skip_api_build: false
azure_static_web_apps_api_token: '<DEPLOYMENT_TOKEN_HERE>'

Because I changed the api to the apps folder I need to provide this in the AzureStaticWebApp configuration under `api_location`. Also, replace <DEPLOYMENT_TOKEN_HERE> with your copied token.

Reminder to also change the app_build_command to ‘npm run build’, which is referencing the earlier created build script for our blog.

Saving and running this pipeline will do several things in order:

  1. Install dependencies (npm ci)
  2. Run linter for only affected pieces of code through NX (nx affected lint)
  3. Run tester for only affected pieces of code through NX (nx affected test)
  4. Run build for only affected pieces of code through NX (nx affected build)
  5. Deploy to SWA

You can now navigate to your SWA and check to see if your application loads and also call the API behind it (which will be deployed in a management environment under the /api path):

https://<URL_TO_SWA>/api/<name of your function>?name=Tim

Running this locally

Cool, so now we can deploy and test our application in the cloud. But that means that every change It needs to be deployed before I can validate if it works properly. That’s where the Azure Static Web App CLI and Azure Function CLI comes in handy. You can use this by following this tutorial

Sadly I had to use Node16 because there is an open issue about connecting to the Azure Function API in Node18 or higher (https://github.com/Azure/static-web-apps-cli/issues/598).

After installing the CLI I also added two more scripts to my packages.json

 "start": "npx nx run blog:serve",
"swa": "swa start http://localhost:5173/ --run \"npm run start\" --api-location ./apps/blog-api",

First I’m telling my CLI to start my blog App and the ‘swa’ is just handy so I can run npm run swa instead of typing in the entire URL everytime. After these changes, I start my app with “npm run swa” and browse: http://localhost:4280/

This simulates an Azure Static Web App locally including the managed Azure Function layer which provides you with the ability to use the build-in /.auth route and /api route.

Authentication

I really love the build-in authentication for Azure AD in SWA, there is a whole tutorial about configuring this properly for either Azure AD, GitHub or Twitter, but Also custom-authentication.

But I want to show you how to explicitly restrict a single Azure AD domain (Tenant) for our SWA, because I don’t want EVERYONE to log on my application, only the customer that the app is build for. We do this by providing a configuration file named staticwebapp.config.json.

{
"routes": [
{
"route": "/*",
"allowedRoles": ["authenticated"]
}
],
"responseOverrides": {
"401": {
"statusCode": 302,
"redirect": "/.auth/login/aad"
}
},
"navigationFallback": {
"rewrite": "/"
},
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"clientIdSettingName": "AZURE_CLIENT_ID",
"clientSecretSettingName": "AZURE_CLIENT_SECRET"
}
}
}
},
"globalHeaders": {
"Cache-Control": "no-cache"
}
}

Make sure to replace <TENANT_ID> with your Azure Active Directory tenant ID.

The AZURE_CLIENT_ID and AZURE_CLIENT_SECRET is a variable here that needs to be added to the SWA in the portal in Application settings. You may also choose to store your secrets in Azure Key Vault.

Getting these values if by creating a App registration in Azure Portal — Azure AD. Follow these steps:

The redirect URI should be something like https://nice-island-08f774903.3.azurestaticapps.net/.auth/login/aad/callback

In Certificates & Secret you can create a secret to provide in the AZURE_CLIENT_SECRET.

The cool part is that you can also run this locally! By going to your app while running npm run swa, you can automatically be redirected to a mockup authentication page providing you with a access token

Login and enjoy!

--

--