Angular and NestJS Monorepo with GitHub Actions and Docker


In this blog post, we will walk through the process of setting up a monorepo using npm workspaces for an Angular frontend and a NestJS backend. We will also set up a continuous integration and deployment (CI/CD) pipeline using GitHub Actions and Docker.

Setting Up the Monorepo with npm Workspaces

First, let’s set up a monorepo structure using npm workspaces. We’ll have separate directories for our frontend ( Angular), backend (NestJS), and shared code (in my case, DTOs).

Step 1: Create the Project Directory

Create a new directory for your project:

mkdir my-project
cd my-project

Step 2: Initialize npm and Set Up Workspaces

Initialize an npm project and set up workspaces:

npm init -y

Edit the generated package.json to include the workspaces configuration:

{
  "name": "my-project",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "frontend",
    "backend",
    "shared"
  ],
  "scripts": {
    "build:frontend": "npm run build --workspace frontend",
    "build:backend": "npm run build --workspace backend",
    "build": "npm run build:frontend && npm run build:backend"
  }
}

Step 3: Set Up Angular, NestJS, and Shared Directories

Frontend

Create the Angular project:

npx -p @angular/cli ng new frontend --directory frontend

Backend

Create the NestJS project:

npx -p @nestjs/cli nest new backend --directory backend

Shared Directory

Create the shared directory for common DTOs and interfaces:

mkdir shared
cd shared
npm init -y
tsc --init

Step 4: Add a Shared DTO

Create a shared DTO file:

// shared/dtos/user.dto.ts
export interface UserDto {
    id: number;
    name: string;
    email: string;
}

Step 5: Configure TypeScript Paths

Update tsconfig.json in both frontend and backend to include the shared directory:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@shared/*": [
        "../shared/*"
      ]
    }
  }
}

Step 6: Install Shared Package in Frontend and Backend

Run the following commands to install the shared package in both frontend and backend:

cd frontend
npm install ../shared --save
cd ../backend
npm install ../shared --save
cd ..

Setting Up Docker

Next, we will create Dockerfiles for both the frontend and backend applications.

Frontend Dockerfile

Create a Dockerfile in the frontend directory:

# Stage 1: Build the Angular application
FROM node:20-alpine AS build
WORKDIR /workspace

# Copy the entire workspace
COPY . .

# Install dependencies using the workspace root package-lock.json
RUN npm ci

# Build the Angular application
RUN npm run build:frontend

# Stage 2: Serve the application with Nginx
FROM nginx:alpine

# Copy the built Angular application from the previous stage
COPY --from=build /workspace/frontend/dist/frontend /usr/share/nginx/html

# Nginx configuration
COPY --from=build /workspace/frontend/nginx.conf /etc/nginx/conf.d/default.conf

# Expose port 80
EXPOSE 80

# Start Nginx
CMD ["nginx", "-g", "daemon off;"]

Backend Dockerfile

Create a Dockerfile in the backend directory:

###################
# BUILD FOR PRODUCTION
###################

FROM node:20-alpine AS build

# Set working directory
WORKDIR /workspace

# Copy the entire workspace
COPY . .

# Install dependencies using the workspace root package-lock.json
RUN npm ci

# Build the NestJS application
RUN npm run build:backend

###################
# PRODUCTION
###################

FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy the bundled code from the build stage to the production image
COPY --from=build /workspace/backend/dist /app/dist
COPY --from=build /workspace/backend/package.json /app/package.json
COPY --from=build /workspace/node_modules /app/node_modules

# Start the server using the production build
CMD ["node", "dist/main.js"]

EXPOSE 8000

Setting Up GitHub Actions for CI/CD

Let’s set up a GitHub Actions workflow to build and deploy both the frontend and backend Docker images.

GitHub Actions Workflow

Create a workflow file at .github/workflows/deploy.yml:

name: Deploy Frontend and Backend

on:
push:
branches: [ main ]

jobs:
build-and-deploy-frontend:
runs-on: ubuntu-latest

  steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Login to GitHub Container Registry
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Docker meta
      id: meta-frontend
      uses: docker/metadata-action@v4
      with:
        images: ghcr.io/${{ github.repository }}/frontend
        tags: latest

    - name: Build and push frontend
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./frontend/Dockerfile
        push: true
        tags: ${{ steps.meta-frontend.outputs.tags }}

  build-and-deploy-backend:
    runs-on: ubuntu-latest
    needs: build-and-deploy-frontend

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Docker meta
        id: meta-backend
        uses: docker/metadata-action@v4
        with:
          images: ghcr.io/${{ github.repository }}/backend
          tags: latest

      - name: Build and push backend
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./backend/Dockerfile
          push: true
          tags: ${{ steps.meta-backend.outputs.tags }}

Explanation

  1. prepare Job:

    • Checks out the code from the repository.
    • Logs in to the GitHub Container Registry.
  2. build-and-deploy-frontend Job:

    • Checks out the code again.
    • Generates Docker metadata for the frontend image.
    • Builds and pushes the frontend Docker image.
  3. build-and-deploy-backend Job:

    • Depends on the prepare job to ensure the code is checked out and the registry login is successful.
    • Checks out the code again.
    • Generates Docker metadata for the backend image.
    • Builds and pushes the backend Docker image.

Conclusion

In this blog post, we’ve set up a monorepo using npm workspaces for an Angular frontend and a NestJS backend. We created Dockerfiles for both applications and configured a GitHub Actions workflow to build and deploy Docker images for both the frontend and backend. This setup provides a robust CI/CD pipeline for developing and deploying your applications.

Feel free to customize the workflow and Dockerfiles according to your project’s needs.