FullStack application on AWS using Ionic/Angular Frameworks inside an Nx Monorepo with PNPM and Amplify.

Andres Solorzano
21 min readNov 28, 2023


It has been some time since my last tutorial about the front-end side. My previous tutorial discussed the implementation of a Java-native Lambda function in AWS. The function was built using Spring Cloud Functions (SCF) and GraalVM. Furthermore, the project has a Java native API built with Spring Boot 3 and GraalVM. The idea is that our API service emits an event to the EventBridge service, which routes the event messages to the Lambda function. These architectures are known as Event-Driven Architecture (EDA), but in practice, they are called Event-Driven Applications (also EDA) when implemented.

Talking about EDA, it’s well-known that all Serverless architectures are EDA, but not the other way around. Concerning our project, we can ensure it is also serverless and uses some serverless services like Fargate, ELB, Lambda, EventBridge, and SQS.

But the missing piece in this puzzle is the UI component, and continuing with the Serverless way, it’s the turn of the Amplify service to participate in this solution architecture. So let’s get started.


To complete this guide, you’ll need the following tools:

NOTE: You can download the project’s source code from my GitHub repository, which contains all the configurations made in this tutorial.

Depuring Node.js installation (Optional)

If you’re in a situation like mine, where you installed some UI libraries or tools months ago, it’s time to depurate our system. So, I’ll start removing the entire Node.js installation from my system. Also, I want to solve issues related to the lack of access permission when installing NPM packages globally. I hope to have a cleaner installation of Node.js in my system.

The idea is to have a version manager like SDKMAN where you can install the required versions of OpenJDK, Maven, or GraalVM. This tool is called NVM (Node Package Manager).

First, let’s depurate our Nodejs installation:

$ brew uninstall npm
$ brew uninstall node

If you used a GUI installer from the Nodejs site, then execute:

$ sudo rm /usr/local/bin/node
$ sudo rm /usr/local/bin/npm

Remove the “node_modules” from your system:

$ sudo rm -rf /usr/local/lib/node_modules

Remove the “~/.npm” and “~/.node-gyp” folders from your home directory, as they contain npm-related data:

$ rm -rf ~/.npm ~/.node-gyp

Verify that Node and NPM have been removed successfully:

$ which npm
$ which node

Now, let’s install the NVM tool in our systems.

NOTE: When we write these lines, the latest version is 0.39.5:

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash

Open a new terminal window and execute the following command to install the latest LTS version:

$ nvm install --lts

You must see an output like this:

As you can see, the latest LTS version is 18.18.0 of NodeJS and 9.8.1 of NPM. So now, we can install the required libraries for our project.

Package management with PNPM.

The JavaScript ecosystem boasts various package manager tools that aid developers in managing and maintaining their project dependencies. Among these, npm and yarn have been the dominant players for a long time. However, a tool that has gained attention and adoption in recent years is pnpm.

PNPM is a fast, efficient, and deterministic package manager for JavaScript. While it uses the same package.json file, its approach to package management is unique.

The principal advantage I like most is that pnpm does not duplicate packages. Instead of downloading packages repeatedly for each project, PNPM maintains a single copy of each package in a global cache and then links to these packages in node_modules. This results in considerable disk space savings and faster installations.

PNPM is an excellent alternative to traditional package managers. For large projects like monorepos, PNPM is undoubtedly a tool to consider. So, let’s install and configure it:

$ npm install -g pnpm
$ pnpm setup

So, next, let’s try to use it with the Nx tool.

Using Nx 17 for Ionic/Angular Monorepo.

Nx is a set of extensible dev tools for monorepos. Monorepos provides a single repository to house multiple projects (like apps and libraries), making sharing and managing code across different applications easier. While monorepos have been around for a while and are used by big tech companies like Google and Facebook, Nx brings this capability to every organization, optimizing the process and offering best practices.

Teams can easily share configurations, components, utilities, and more by organizing multiple applications and libraries within a single repository. This eliminates the redundancy of code and ensures consistency across applications. When a core component is updated, the change can seamlessly propagate across all projects that use it, providing uniformity and reducing potential errors.

That’s why I created a parent Maven POM for the back-end projects. So, I found good references when I searched for dependency management tools for web projects, maintaining a single version policy, as we did using Maven for our back-end projects.

The idea in this section is to start an Nx project from scratch and then incrementally add the Ionic/Angular components from our previous project.

So, let’s begin installing the required tools globally:

$ pnpm install -g nx@latest

Then, create an empty monorepo using Nx with PNPM as a package manager:

$ npx create-nx-workspace@latest  \
--name city-tasks \
--preset=empty \
--packageManager=pnpm \
--skipGit=true \

After the monorepo is completed, move to the new project directory and install an Nx extension to generate Ionic/Angular projects:

$ cd city-tasks
$ pnpm add --save-dev --save-exact @nxext/ionic-angular

Now we can create a new Ionic/Angular project using the “sidemenu” template:

$ nx generate @nxext/ionic-angular:app  \
--name=city-tasks-app \
--template=sidemenu \
--unitTestRunner=jest \
--e2eTestRunner=cypress \
--linter=eslint \

When using the “— directory=apps” flag, I got the following error:

For this reason, I didn’t use that flag in the previous command, so we need to configure this manually.

Now, open the project using your favorite IDE to see the created files so far:

So, create the apps directory manually and move the app projects inside that directory.

You must edit the “/apps/city-tasks-app/project.json” file, updating directory paths to the new route:

"name": "city-tasks-app",
"sourceRoot": "apps/city-tasks-app/src",
"targets": {
"build": {
"options": {
"index": "apps/city-tasks-app/src/index.html",
"main": "apps/city-tasks-app/src/main.ts",
"tsConfig": "apps/city-tasks-app/tsconfig.app.json",
"assets": [
"styles": [
"input": "./apps/city-tasks-app/src/theme/variables.scss"
"scripts": []
"defaultConfiguration": "production"
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [

Accomplish the same for the “end-2-end” project. Also, look at the rest of the TS configuration files, like the “tsconfig.json” file and others.

Let’s try to build and deploy our new app locally, executing the following commands from the project’s root directory:

$ nx build city-tasks-app
$ nx serve city-tasks-app

You must see a message like the following after running the last command:

So, open a new browser tab with the indicated URL to see what happens:

So our Ionic/Angular sidemenu template is working. But before migrating the last City Task application’s components into the new repository, let’s compare the migration tasks we must perform.

City Tasks Front-end dependencies.

Examining the City Tasks App project documentation, I saw that we must install the following dependencies:

$ pnpm add aws-amplify
$ pnpm add angular-oauth2-oidc
$ pnpm add animate.css
$ pnpm add date-fns
$ pnpm add date-fns-tz

Also, we must install the NgRx framework, and as we’re using Nx in our new project, we must execute the following commands:

$ pnpm add @ngrx/store@latest          && pnpm exec nx g @ngrx/store:ng-add
$ pnpm add @ngrx/store-devtools@latest && pnpm exec nx g @ngrx/store-devtools:ng-add
$ pnpm add @ngrx/effects@latest && pnpm exec nx g @ngrx/effects:ng-add
$ pnpm add @ngrx/router-store@latest && pnpm exec nx g @ngrx/router-store:ng-add
$ pnpm add @ngrx/data@latest && pnpm exec nx g @ngrx/data:ng-add

NOTE: You can review my previous tutorial when I wrote about NgRx and implemented it in the City Tasks App. That tutorial was written last year when we hadn’t implemented the IdP yet. So please review the topics related to the NgRx concepts only.

Now, install the Amplify CLI tool in our system:

$ pnpm add -g @aws-amplify/cli

All previous commands install the required dependencies and update the package.json file in the project’s root folder.

When I executed these commands, I got a warning message indicating that some dependencies were outdated. To fix this issue, you must run the following Nx commands:

$ npx nx migrate latest
$ npx nx migrate @angular/core@latest
$ npx nx migrate @ngrx/store@latest

Finally, run the following command to execute the migrations:

$ pnpm exec nx migrate --run-migrations 

You can also execute the following command to list the outdated packages:

$ pnpm outdated

You can update the required packages using the PNPM command:

$ pnpm update eslint@latest prettier@latest 

We have installed all project dependencies to migrate the existing City Task App components to the new Nx project monorepo. But before, let’s analyze the instructed steps before the migration.

Angular 17 — New Features.

Before migrating the existing City Tasks components into the new project monorepo with Nx, let’s examine the following Angular guide, which provides some considerations to take into account during the migration:

We need to analyze all the implications of the migrations from Angular version 14 to 15 before going directly to Angular 17. We must repeat this process from 15 to 16 and 16 to 17.

Let’s describe some essential features of Angular 17:

  • Signals.
  • Standalone Components.
  • New Template Syntax.
  • End-to-End testing.
  • Ahead-of-Time (AOT) Compilation.
  • EsModules (ViteJS).

I don’t cover all these features here because this can become an hour-long tutorial. I promise to work on all these features in another tutorial, writing specifically on Angular 17. Our objective in this tutorial is to build a full-stack monorepo and use the tools we have been working on to deploy our applications and services into AWS.

So, let’s continue with the next steps of the tutorial.

Amazon Cognito as Identity Provider (IdP).

I’ve created a Git repo called “hiperium-city-idp,where you can find the Amplify commands to configure the IdP service. Remember that I’m not sharing my “.amplify” directory, so you must execute the following commands.

NOTE: Following my previous tutorials, you might have configured and deployed the IdP service in AWS. You can skip this section or continue reading if you want to validate your configurations.

First, log in to the SSO using the helper shell script. Use your AWS profile for your Identity Provider account:

$ hiperium-login

Then, navigate to the cloned “hiperium-city-idp” repository and create a new branch for the pre-production environment:

$ git checkout -b pre

Initialize the Amplify project for this new environment:

$ amplify init

These are the initial config values that I’m using. Feel free to use the same or your own:

NOTICE: idp-pre is the AWS profile I used for this configuration, and pre is the Amplify environment name.

So now, it’s time to configure our Amplify Authentication module.

$ amplify add auth

These are the initial config values that I’m using:

NOTE: I enabled Multi-Factor Authentication (MFA) for our IdP. This is not mandatory for this tutorial, but if you want details about this configuration, please review my previous article.

It’s important to mention the following settings regarding the previous configuration:

  • I used <localhost> for OAuth redirections. In the following sections, we will update this property by adding the Amplify URL to redirect the user after a successful login when our app is deployed on AWS.
  • I also used <https://oauth.pstmn.io/v1/browser-callback/> for sign-in redirection to allow Postman to access our API endpoints using a valid JWT access token.

Finally, go to the cli-inputs.json file in the “amplify/backend/auth/CityIdP directory and update the “userpoolClientGenerateSecret” property to true to generate a client secret to be used in the Postman tool:

"aliasAttributes": [],
"userpoolClientGenerateSecret": true,
"userpoolClientRefreshTokenValidity": 30,

Finally, push these changes into your AWS account using the Amplify CLI:

$ amplify push

You must see the following message, which indicates a successful deployment:

So, we have deployed our IdP service into AWS. The next step is updating the City Tasks components for local deployment.

Running Back-end as a Standalone service.

Before running the Ionic/Angular app, we must update the <envoy.yaml> file to allow connections from our Ionic/Angular application. So, clone the <city-tasks-lambda-native-with-dynamodb> project, which has the latest version of our back-end services.

Execute the hiperium-login command again using your <idp-pre> profile to get a valid AWS session before obtaining the IdP endpoint by the automation scripts.

IMPORTANT: editing our “/etc/hosts” file is unnecessary to redirect all HTTPS communication to the local API microservice. We are now configuring our front-end application to make accurate functional tests.

Deploy the back-end services by executing the following shell script:

$ ./run-scripts

Select option 1 to deploy the back-end using Docker Compose:

Review the Docker Compose logs for the Task API service and validate that it’s running on port 8080 without errors among the other services:

Open a new terminal and navigate to the back-end project directory to test if the function deployed on the Docker container is working:

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d @apis/city-events-function/src/test/resources/events/lambda-event-valid-detail.json

You must see the following output in the Docker Compose terminal tap:

Open Postman and execute the following API call to get all created tasks. Recall to configure the Authorization properties to get a valid JWT token from the IdP service:

So, as our Events Lambda function and the Tasks API microservice are working as expected, let’s integrate our Ionic/Angular app to make functional tests.

Running Ionic/Angular as a Standalone service.

Go to your AWS Cognito service console and copy the User Pool ID and Client Web ID values. Paste them into the <environment.ts> file located in the front-end project as follows:

export const authConfig: AuthConfig = {
issuer: 'https://cognito-idp.<AWS_REGION>.amazonaws.com/<YOUR_COGNITO_USER_POOL_ID>',
oidc: true,
strictDiscoveryDocumentValidation: false,
redirectUri: 'http://localhost:4200/home/',
responseType: 'code',
scope: 'phone email openid profile',
showDebugInformation: !environment.production

In the same file, paste the following code to create a global variable that configures the NgRx Data to connect to our back-end service:

export const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: 'https://localhost/api/v1',
timeout: 3000,
delete404OK: false,
trailingSlashEndpoints: false,

Execute the following command inside the Nx monorepo to build and run our migrated Ionic/Angular app:

$ nx build city-tasks-app
$ nx serve city-tasks-app

The last command prints the address to access our application. Open that URL in a new browser tab, and you must see the following home page:

If you click the <Login> button, you must be redirected to the Cognito IdP service page to enter your credentials:

Enter your credentials if you have created one in your IdP service, or click the <Sign up> link to create your account. As we mentioned before, I enabled the MFA feature, so when you create your account, you must follow the process as shown following:

When you create your user and enter the credential, you must have access to the home page of our application:

Click on the <Task> option in the left menu to try to get the created tasks in the database, but you must have the following error in developers tools in your browser window:

This is an issue with our self-signed TLS certificate. Accessing the Spring Boot Actuator page in another tab is the easiest way. The browser must ask you to access an insecure site:


Proceed to the unsecured site, and you must get a status message from the Spring Boot Actuator endpoint:

Now, your browser will trust this URL, and in the subsequent calls, you won’t have a certification error by the browser. So, return to the Ionic/Angular app browser tab and refresh the page. It would be best if you did not have errors in the browser console because you’re again on the home page. Now refresh the app and click again on the <Task> option to see the results:

Now we have a CORS problem. To fix this, stop the docker-compose process and update the Envoy configuration to use CORS in the filters section as shown next:

name: local_route
- name: local_api
domains: [ "*" ]
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
- prefix: "*"
allow_headers: "keep-alive,user-agent,cache-control,content-type,access-control-request-headers,access-control-request-method,authorization,x-requested-with"
allow_credentials: true
max_age: "1728000"

Start one more time the docker-compose cluster and try again to access the Tasks page:

Excellent. So now, it’s time to unify our repositories into a single one.

Unifying Nx Monorepo with Back-end services.

Copy both projects in a single one. I called this monorepo <city-tasks-aws-fullstack>, which you can find in my GitHub account. The project structure will have the following:

To follow the new Monorepo conventions, I renamed the old <src> folder to <apis>, as seen in the previous image. So, the <apps> folder is dedicated to front-end applications, and the <apis> folder will be focused on back-end services.

Try to build and deploy the entire solution using this new convention to see if all is working as expected before continuing with the next section. Recall to rename the script files that use the old “src” directory to use the actual one.

Docker Compose for local Deployment.

The service that needs to be added is the City Tasks Application. So first, create the Dockerfile inside the <city-tasks-app> directory with the following content:

######################## Stage 1 : Builder Image #########################
FROM node:18 AS builder
WORKDIR /workspace

RUN npm install -g pnpm
COPY pnpm-lock.yaml ./
COPY package.json ./
RUN pnpm install

COPY . .
RUN pnpm run build

######################## Stage 2: Application Image ######################
FROM nginx:alpine
COPY --from=builder /workspace/dist/city-tasks-app /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

Next, update the <compose.yaml> file adding the City Tasks App service:

image: hiperium/city-tasks-app:1.8.0
container_name: tasks-app
context: .
dockerfile: apps/city-tasks-app/Dockerfile
- "80:80"
- NODE_ENV=production
- city-image-builder
- city-tasks-postgres
- city-tasks-api
- city-tasks-api-proxy
- city-network

Then, modify the <environment.ts> file, updating the API address with the one specified in the Docker Compose file:

export const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: 'https://localhost/api/v1',

We must add a new redirection URI in our Cognito OAuth configuration. For this purpose, update the IdP project using the Amplify command:

$ amplify update auth

Select the “Add/Edit signin and signout redirect URIs” option, and add the new hostname address like this:

Push the changes to AWS using the Amplify command:

$ amplify push

Modify again the <environment.ts> file, updating the <redirectUri> property to use the new one:

export const authConfig: AuthConfig = {
redirectUri: 'http://localhost/',

Execute the Docker Compose to deploy the entire solution locally:

$ docker compose up --build

Validate that all services are deployed successfully, and then try to open the City Tasks App in your browser. You must access the app without errors:

Excellent. Our entire solution was deployed using Docker Compose so we could test our changes locally before pushing them to AWS.

Deploying Back-end Services on AWS Fargate.

For this purpose, use our main script to deploy all back-end services automatically (ECS cluster and Lambda function):

$ ./run-scripts

Select option 2 to deploy the back-end into AWS:

Enter the required information as the script asks before deploying the required infra and services into AWS.

If you want to know the entire deployment process, please go to my previous tutorial and find section 13 to see the details that apply to this section.

Deploying Front-end Services on AWS Amplify.

After the back-end deployment has been finished into AWS, go to the <city-tasks-app> directory and execute the following command to init the Amplify project in AWS:

$ amplify init

Enter the required general information about your project. I used the following information, calling Amplify the project as <CityTasksApp>:

Verify that the project was created in the Amplify console:

Update the <environment.ts> file with the required information. The following is a content template that you must fulfill:

export const defaultDataServiceConfig: DefaultDataServiceConfig = {
root: 'https://ALB_API_ENDPOINT/api/v1',

export const authConfig: AuthConfig = {
issuer: 'https://cognito-idp.IDP_AWS_REGION.amazonaws.com/COGNITO_USER_POOL_ID',
redirectUri: 'https://main.AMPLIFY_APP_ID.amplifyapp.com/',

NOTE: You can get the ALB endpoint when you deploy the back-end services. Besides, the Amplify ID can be obtained using the following command:

$ aws amplify list-apps                         \
--query "apps[?name=='CityTasksApp'].appId" \
--output text \
--profile tasks-dev

Now update the <main.ts> file, adding the following configuration following the steps indicated by Amplify in its official documentation:

import amplifyConfig from "./amplifyconfiguration.js";


if (environment.production) {
ConsoleLogger.LOG_LEVEL = "INFO";

Push all your pending Ionic/Angular project changes to your Git repository. This is because we will be configuring the Amplify Hosting with the latest version of your app. So make sure you push your changes into your Git repository:

$ git add .
$ git commit -m "Updating Ionic/Angular config for Amplify"
$ git push

Then, execute the following command to configure your app in the Amplify Hosting service. This command will ask you for additional information before continuing with the following steps:

$ amplify add hosting

You must select the following options:

  • Hosting with Amplify Console.
  • Continuous deployment.

The last one is necessary to allow Amplify to access our Git repo and download the code before publishing the changes. This is a CI/CD process, but for the moment, we’re using only 1 environment.

You’ll be directed to the Amplify console when the command opens a browser window or tab. Select the <Hosting environments> tab, and select the Git Provider where your code resides:

In the next screen, you will be asked to allow Amplify to connect to your Git repository, so click on the <Authorize> button to continue:

After authorizing Amplify to access your Git repository, you will be directed to a steeper form to complete the hosting configuration.

In step 1, select your Git repo and the branch where your code resides:

In step 2, select the Amplify app name, the registered environment, and the IAM Role used by Amplify to deploy the required infra. I created this role in a previous tutorial, so click on the <Create new role> if you don’t have this role created yet:

In step 3, review your selected settings before proceeding:

Finally, after clicking on the <Save and deploy> button, a CI/CD pipeline will be started by Amplify:

But in the <Build> phase, I got the following error:

Line 4 indicates the missing <angular.json> file, which is the main config file when you create an Angular project using the Ng tool. However, we used Nx to create our monorepo and the Ionic/Angular project. It is a bug or some misunderstood configuration by Amplify.

For this reason, I created the following basic <angular.json> file in the project’s root directory:

"version": 1,
"projects": {
"city-tasks-app": {
"root": "apps/city-tasks-app",
"projectType": "application",
"architect": {}

If you push this change into your Git repository, you will see that the < back-end> build runs well, so we solve the problem at that stage, but you will see a new error now in the < front-end> phase:

If you try to build the project locally, you will see the same error:

The new <angular.json> file is causing this error. So, an alternative to fix this error is to create the <angular.json> file on the fly when the < back-end> phase is executing and then delete it in the < front-end> phase.

So, update our <amplify.yml> file must contain the following content:

- appRoot: apps/city-tasks-app
- echo '{"version":1,"projects":{"city-tasks-app":{"root":"apps/city-tasks-app","projectType":"application","architect":{}}}}' > angular.json
- amplifyPush --simple
- rm angular.json

If you push these changes into your Git repository, you will receive another error:

Notice the error indicates the missing <amplifyconfiguration.json> in <main.ts> file. That configuration file was created by the Amplify CLI when we initiated our project, but this command also created the following <.gitignore> file:


So, to solve this new problem, we have 2 options. The easiest way is to update the <.gitignore> file, removing the needed file. The other option is to update the <amplify.yml> file again, adding the missing file on the fly:

- appRoot: apps/city-tasks-app
buildPath: /
- npm install -g pnpm
- pnpm install
- echo '{"aws_project_region":"us-east-1" }' > apps/city-tasks-app/src/amplifyconfiguration.json

I will implement this last solution because the recommendation is to keep sensitive information private in configuration files if you work on a private/company project. So, as our project is public, sharing information about AWS accounts is not recommended.

So, pushing all these changes finally runs successfully in our pipeline:

Clicking on the generated Amplify URL for our project, you must see the main page of our Ionic/Angular app:

Before clicking the <Login> button, go to the <hiperium-city-idp> project and update the OAuth settings for redirection URIs. Recall this project is also using Amplify, so you need to execute the following command to update the auth module:

$ amplify update auth

Then, select the <Add/Edit signin and signout redirect URIs> option and enter your generated Amplify Hosting endpoint. Push your changes to the Amplify service for your new configuration to take effect:

Notice that the generated URL uses the <amplify.com> domain, the branch name of our Git repository, which is <main>, and a secure communication with SSL/TLS.

Now, you can click the <Login> button to be redirected to the IdP login page and follow the known process we did before to access the City Tasks home page.

We have finally deployed our app in the Amplify Hosting service. So, the last step of our tutorial is to create the required automation scripts to deploy the entire solution.

Automation Scripts.

As usual, I created script files to create/delete the required infra on AWS to deploy our fullstack City Task application. So, our new menu is the following:

Use option 1 to deploy the fullstack City Tasks solution on your local machine using Docker Compose, as we did initially in this tutorial. Before deploying the entire City Task application in your AWS accounts, I recommend doing this.

The other new options are to create and delete front-end stacks in or from your AWS workloads account. Those options are the main topics in this tutorial.

Finally, I added 2 other options to create or delete the back-end and front-end services using a single script instead of creating all those services independently. So you’re free to choose whatever option you like regarding your learning in AWS or development journey ;)

And that’s it!! It is a great pleasure to write about these kinds of articles. I hope you learn as much as I do and apply this knowledge to your projects.

In my following tutorial, I’ll write about implementing this fullstack application on AWS but using CI/CD pipelines with the help of the AWS tools. So I will see you then.