Skip to content

Part 3: Going Full-Stack

Add a Vue 3 frontend to your Spring Boot API with multi-container builds and inter-service URL resolution.

What you'll learn

  • Building a Vue 3 SPA with Vite
  • Configuring multi-container builds in appmecca.yaml
  • Using ${service:backend} for automatic backend URL injection
  • Serving the frontend with nginx

What you'll need

  • A working todo API from Part 2
  • Node.js 20+ installed locally (for frontend development only)

Project structure

Reorganize your repository into backend and frontend directories:

todo-app/
  backend/
    src/
    build.gradle
    settings.gradle
    Dockerfile
  frontend/
    src/
    package.json
    vite.config.js
    Dockerfile
  appmecca.yaml

Move your existing Spring Boot code into backend/. The backend Dockerfile stays the same -- just update its context path in appmecca.yaml.

Create the Vue 3 frontend

Initialize the project:

cd frontend
npm create vite@latest . -- --template vue
npm install

Write the main App component

<!-- frontend/src/App.vue -->
<script setup>
import { ref, onMounted } from 'vue'

const API_URL = import.meta.env.VITE_API_URL || ''
const todos = ref([])
const newTitle = ref('')

async function fetchTodos() {
  const res = await fetch(`${API_URL}/api/todos`)
  todos.value = await res.json()
}

async function addTodo() {
  if (!newTitle.value.trim()) return
  await fetch(`${API_URL}/api/todos`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title: newTitle.value })
  })
  newTitle.value = ''
  await fetchTodos()
}

async function toggleTodo(todo) {
  await fetch(`${API_URL}/api/todos/${todo.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...todo, done: !todo.done })
  })
  await fetchTodos()
}

async function deleteTodo(id) {
  await fetch(`${API_URL}/api/todos/${id}`, { method: 'DELETE' })
  await fetchTodos()
}

onMounted(fetchTodos)
</script>

<template>
  <div class="container">
    <h1>Todo App</h1>
    <form @submit.prevent="addTodo">
      <input v-model="newTitle" placeholder="What needs doing?" />
      <button type="submit">Add</button>
    </form>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" :checked="todo.done" @change="toggleTodo(todo)" />
        <span :class="{ done: todo.done }">{{ todo.title }}</span>
        <button @click="deleteTodo(todo.id)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<style>
.done { text-decoration: line-through; opacity: 0.6; }
</style>

Write the frontend Dockerfile

Build the Vue app with Node, then serve the static files with nginx:

# frontend/Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG VITE_API_URL
ENV VITE_API_URL=${VITE_API_URL}
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 3000

Add the nginx config

# frontend/nginx.conf
server {
    listen 3000;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

SPA routing

The try_files directive ensures that client-side routes like /about fall back to index.html instead of returning a 404. This is essential for any single-page application.

Update appmecca.yaml

Configure both containers and use ${service:backend} to inject the backend URL at build time:

database:
  enabled: true

containers:
  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/
    args:
      VITE_API_URL: "${service:backend}"

deploy:
  - name: backend
    container: backend
    port: 8080
    health_check:
      path: /health

  - name: frontend
    container: frontend
    port: 3000
    health_check:
      path: /

Service URL resolution

${service:backend} resolves to the public HTTPS URL of the backend service, for example https://todo-app-backend-yourgroup.dev.example.com. AppMecca resolves this at build time and passes it as the VITE_API_URL build argument.

A typo is caught early — the validator rejects a ${service:...} reference to a service that is not declared in the same deploy: block.

Build and deploy

mecca build trigger -g git@github.com:yourorg/todo-app.git
mecca app deploy todo-app

AppMecca builds both containers in the same build run, pushes both to its private image registry, then deploys them together. Each service gets its own hostname on the platform HTTPS router.

Verify the deployment

Check that both services are running:

mecca app show todo-app

View service logs

Read the container output for a specific service. The service is selected with --service, not as a positional argument:

mecca logs todo-app --type container --service frontend
mecca logs todo-app --type container --service backend

Use --since to widen or narrow the time window (the default is the last hour), and --level to filter:

mecca logs todo-app --type container --service backend --since 30m
mecca logs todo-app --level error
mecca logs todo-app --search "timeout"

Test the full stack

Open the frontend URL from mecca app show todo-app. You should see the todo app with a form to add items. Add a few todos and verify they persist across page reloads -- they are stored in PostgreSQL via the backend API.

How multi-container deployment works

When AppMecca deploys a multi-container app:

  1. It provisions a route on the platform HTTPS router for each service that declares a port
  2. Each service gets its own URL — always including the service name
  3. The containers are pulled and started on AppMecca hosts
  4. Health checks confirm each service is responding before traffic is switched to it
  5. The deployment status moves to completed
https://todo-app-frontend-yourgroup.dev.example.com  -->  nginx (port 3000)
https://todo-app-backend-yourgroup.dev.example.com   -->  Spring Boot (port 8080)

Command reference

Command Purpose
mecca app show <app> List all services with status and URLs
mecca logs <app> --type container --service <svc> Container output for one service
mecca logs <app> --since 30m Logs from a time window
mecca logs <app> --level error Errors only, across every source

What's next

In Part 4: SPA Runtime Config, you will replace the build-time API URL with runtime configuration, so the same container image works in any environment without rebuilding.