cd..blog

Integrated Full-Stack: Vue, Koa, & DigitalOcean with TypeScript

const published = "Oct 9, 2025, 05:26 AM";const readTime = 7 min;
vuejskoadigitaloceantypescriptcicd
Master full-stack development by integrating Vue.js, Koa, and DigitalOcean. This guide covers monorepos, modern tooling, advanced testing, and CI/CD with TypeScript for robust applications.

Modern web development demands a harmonized approach across the entire stack. Integrating frontend, backend, package management, testing, and deployment into a cohesive workflow is paramount for delivering scalable, maintainable, and high-quality applications. This article delves into building "Project Horizon," a full-stack application leveraging Vue.js for the frontend, Koa for the backend, all powered by TypeScript, and deployed seamlessly on DigitalOcean.

Architecting for Integration: Monorepo with pnpm Workspaces

A monorepo structure offers significant advantages for full-stack projects, particularly when sharing code like types or utility functions between frontend and backend. We'll use pnpm workspaces for efficient dependency management, deduplication, and faster installs, a significant improvement over traditional npm or yarn for large codebases.

Project Structure

.nvmrc
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.base.json
packages/
├── backend/
│   ├── src/
│   ├── tsconfig.json
│   ├── package.json
│   └── Dockerfile
├── frontend/
│   ├── src/
│   ├── tsconfig.json
│   ├── package.json
│   └── Dockerfile
└── shared/
    ├── src/
    ├── tsconfig.json
    └── package.json

pnpm-workspace.yaml

This file defines the workspaces:

# pnpm-workspace.yaml
packages:
  - 'packages/*'

Shared Types

Defining types in the shared package allows both Vue.js and Koa applications to benefit from strong type checking.

// packages/shared/src/types/index.ts
export interface ITask {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
}

export interface ApiResponse<T> {
  data: T;
  message?: string;
  success: boolean;
}

Frontend Excellence with Vue.js & TypeScript

Our frontend will be built with Vue 3, leveraging the Composition API and <script setup> for enhanced readability and maintainability. Vite provides an incredibly fast development experience and optimized builds.

Basic Vue Component with API Interaction

// packages/frontend/src/components/TaskList.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { ITask, ApiResponse } from '@project-horizon/shared';

const tasks = ref<ITask[]>([]);
const isLoading = ref(true);
const error = ref<string | null>(null);

onMounted(async () => {
  try {
    const response = await fetch('/api/tasks'); // Proxy to backend in dev, direct in prod
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const result: ApiResponse<ITask[]> = await response.json();
    if (result.success) {
      tasks.value = result.data;
    } else {
      error.value = result.message || 'Failed to fetch tasks.';
    }
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'An unknown error occurred.';
  } finally {
    isLoading.value = false;
  }
});
</script>

<template>
  <div>
    <h1>Project Horizon Tasks</h1>
    <p v-if="isLoading">Loading tasks...</p>
    <p v-else-if="error" class="error">Error: {{ error }}</p>
    <ul v-else>
      <li v-for="task in tasks" :key="task.id">
        {{ task.title }} - {{ task.completed ? 'Completed' : 'Pending' }}
      </li>
    </ul>
  </div>
</template>

<style scoped>
.error { color: red; }
</style>

Robust Backend with Koa & TypeScript

Koa is a lean, expressive middleware framework for Node.js. Its async/await driven architecture naturally handles asynchronous operations, making it ideal for modern API development. We'll use koa-router for routing.

Koa Application Entry Point

// packages/backend/src/app.ts
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import type { ITask, ApiResponse } from '@project-horizon/shared';

const app = new Koa();
const router = new Router();

app.use(bodyParser());

// Mock data for demonstration
const tasks: ITask[] = [
  { id: '1', title: 'Setup monorepo', completed: true },
  { id: '2', title: 'Implement Vue frontend', completed: false },
  { id: '3', title: 'Build Koa API', description: 'With shared types', completed: false },
];

router.get('/api/tasks', (ctx) => {
  ctx.body = { success: true, data: tasks } as ApiResponse<ITask[]>;
});

router.post('/api/tasks', (ctx) => {
  const { title, description } = ctx.request.body as Pick<ITask, 'title' | 'description'>;
  if (!title) {
    ctx.status = 400;
    ctx.body = { success: false, message: 'Title is required' } as ApiResponse<any>;
    return;
  }
  const newTask: ITask = {
    id: String(tasks.length + 1),
    title,
    description,
    completed: false,
  };
  tasks.push(newTask);
  ctx.status = 201;
  ctx.body = { success: true, data: newTask, message: 'Task created' } as ApiResponse<ITask>;
});

app.use(router.routes());
app.use(router.allowedMethods());

// Basic error handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx);
  ctx.status = err.statusCode || err.status || 500;
  ctx.body = { success: false, message: err.message || 'Internal Server Error' } as ApiResponse<any>;
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Koa server running on http://localhost:${port}`);
});

Comprehensive Testing & Debugging

Robust testing and efficient debugging are non-negotiable for production-ready applications. Our monorepo setup simplifies integrating various testing tools and debugging configurations.

Frontend Testing (Vitest & Playwright)

  • Unit/Component Testing: Vitest is an excellent choice for Vue 3 projects, offering a fast, Vite-native testing experience.
  • End-to-End Testing: Playwright provides reliable cross-browser E2E testing, simulating user interactions directly in a browser.

Backend Testing (Mocha, Chai, Supertest)

  • Mocha as the test runner, Chai for assertions, and Supertest for making HTTP requests to our Koa API without actually starting a server.
// packages/backend/src/tests/api.test.ts
import request from 'supertest';
import { expect } from 'chai';
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
// Import app directly for testing or create a testable instance
// For simplicity, we'll recreate a minimal app instance for tests.

describe('Task API', () => {
  let app: Koa;
  let server: ReturnType<Koa['listen']>;

  before(() => {
    app = new Koa();
    const router = new Router();
    app.use(bodyParser());

    // Simplified routes for testing
    const tasks = [{ id: '1', title: 'Test Task', completed: false }];

    router.get('/api/tasks', (ctx) => {
      ctx.body = { success: true, data: tasks };
    });

    router.post('/api/tasks', (ctx) => {
      const { title } = ctx.request.body as { title: string };
      if (!title) { ctx.status = 400; ctx.body = { success: false, message: 'Title required' }; return; }
      const newTask = { id: String(tasks.length + 1), title, completed: false };
      tasks.push(newTask);
      ctx.status = 201; ctx.body = { success: true, data: newTask };
    });

    app.use(router.routes());
    app.use(router.allowedMethods());
    server = app.listen(); // Listen on an ephemeral port
  });

  after(() => {
    server.close();
  });

  it('should fetch all tasks', async () => {
    const res = await request(server).get('/api/tasks');
    expect(res.status).to.equal(200);
    expect(res.body.success).to.be.true;
    expect(res.body.data).to.be.an('array');
    expect(res.body.data[0].title).to.equal('Test Task');
  });

  it('should create a new task', async () => {
    const res = await request(server)
      .post('/api/tasks')
      .send({ title: 'New Test Task' })
      .set('Accept', 'application/json');

    expect(res.status).to.equal(201);
    expect(res.body.success).to.be.true;
    expect(res.body.data).to.have.property('title', 'New Test Task');
  });

  it('should return 400 if title is missing', async () => {
    const res = await request(server)
      .post('/api/tasks')
      .send({ description: 'No title' })
      .set('Accept', 'application/json');

    expect(res.status).to.equal(400);
    expect(res.body.success).to.be.false;
    expect(res.body.message).to.equal('Title required');
  });
});

Integrated Debugging (VS Code)

VS Code's multi-root workspace feature allows debugging both frontend and backend concurrently. A launch.json configuration can orchestrate this.

// .vscode/launch.json (root of monorepo)
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Vue Frontend (Vite)",
      "request": "launch",
      "type": "chrome",
      "url": "http://localhost:5173", // Default Vite port
      "webRoot": "${workspaceFolder}/packages/frontend",
      "runtimeArgs": [
        "--remote-debugging-port=9222"
      ],
      "preLaunchTask": "start:frontend-dev"
    },
    {
      "name": "Launch Koa Backend",
      "request": "launch",
      "type": "node",
      "program": "${workspaceFolder}/packages/backend/src/app.ts",
      "cwd": "${workspaceFolder}/packages/backend",
      "runtimeArgs": [
        "-r", "ts-node/register"
      ],
      "env": {
        "TS_NODE_PROJECT": "${workspaceFolder}/packages/backend/tsconfig.json"
      },
      "console": "integratedTerminal",
      "restart": true,
      "protocol": "inspector"
    }
  ],
  "compounds": [
    {
      "name": "Full-Stack Debug",
      "configurations": [
        "Launch Vue Frontend (Vite)",
        "Launch Koa Backend"
      ]
    }
  ]
}

Define start:frontend-dev in your root package.json to run pnpm --filter frontend dev.

Cloud-Native Deployment with DigitalOcean & Docker

DigitalOcean offers a range of services from Droplets (VMs) to managed Kubernetes. For ease of deployment and scalability for mid-sized applications, DigitalOcean App Platform is an excellent choice. We'll containerize our frontend and backend using Docker and automate deployment with GitHub Actions.

Dockerfiles

Backend (packages/backend/Dockerfile):

# packages/backend/Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./ # Copy root pnpm files
COPY packages/backend/package.json ./packages/backend/ # Copy backend package.json
COPY packages/shared/package.json ./packages/shared/ # Copy shared package.json
RUN pnpm install --prod --filter backend # Install only production dependencies for backend
COPY packages/backend/src ./packages/backend/src
COPY packages/shared/src ./packages/shared/src
COPY tsconfig.base.json ./ # Copy root tsconfig
COPY packages/backend/tsconfig.json ./packages/backend/tsconfig.json
COPY packages/shared/tsconfig.json ./packages/shared/tsconfig.json

RUN pnpm --filter backend exec tsc # Compile backend TypeScript

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules # Copy node_modules from builder
COPY --from=build /app/packages/backend/dist ./packages/backend/dist # Copy compiled JS
COPY --from=build /app/packages/backend/package.json ./packages/backend/package.json
ENV NODE_ENV=production
CMD ["node", "packages/backend/dist/app.js"]
EXPOSE 3000

Frontend (packages/frontend/Dockerfile):

# packages/frontend/Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./ # Copy root pnpm files
COPY packages/frontend/package.json ./packages/frontend/ # Copy frontend package.json
RUN pnpm install --filter frontend # Install dependencies for frontend
COPY packages/frontend/src ./packages/frontend/src
COPY packages/frontend/public ./packages/frontend/public
COPY packages/frontend/vite.config.ts ./packages/frontend/vite.config.ts
RUN pnpm --filter frontend exec vite build # Build frontend assets

FROM nginx:alpine
COPY --from=build /app/packages/frontend/dist /usr/share/nginx/html
COPY packages/frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

CI/CD with GitHub Actions and DigitalOcean Container Registry

Automate building, testing, and pushing Docker images to DigitalOcean Container Registry. DigitalOcean App Platform can then pick up these images for deployment.

# .github/workflows/deploy.yaml
name: CI/CD to DigitalOcean App Platform

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  DO_REGISTRY_URL: registry.digitalocean.com/project-horizon-registry

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 8
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run Backend Tests
        run: pnpm --filter backend test

      - name: Run Frontend Tests
        run: pnpm --filter frontend test:unit

  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to DigitalOcean Container Registry
        uses: digitalocean/action-doctl@v2
        with:
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
        run: doctl registry login

      - name: Build & Push Backend Image
        run: |
          docker build -t $DO_REGISTRY_URL/backend:latest -f packages/backend/Dockerfile .
          docker push $DO_REGISTRY_URL/backend:latest

      - name: Build & Push Frontend Image
        run: |
          docker build -t $DO_REGISTRY_URL/frontend:latest -f packages/frontend/Dockerfile .
          docker push $DO_REGISTRY_URL/frontend:latest

      - name: Deploy to DigitalOcean App Platform
        uses: digitalocean/app_platform_deploy@v2
        with:
          app_id: ${{ secrets.DO_APP_ID }}
          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
          # App Platform will automatically detect new image tags in the registry
          # if configured for auto-deploy from registry. 
          # Explicit rebuild/deploy triggered if necessary.

Note: The DigitalOcean App Platform can be configured to watch your Container Registry for new images, triggering deployments automatically. You'd typically define services for your frontend (web service, port 80) and backend (web service, port 3000) within your App Platform specification, pointing them to the respective images. secrets.DIGITALOCEAN_ACCESS_TOKEN and secrets.DO_APP_ID should be set in your GitHub repository.

Best Practices & Actionable Insights

  1. End-to-End Type Safety: Leverage shared TypeScript types rigorously. This catches errors early, improves developer experience, and enhances code quality across the entire stack.
  2. Automated Testing: Integrate unit, component, and E2E tests into your CI/CD pipeline. No code merges to main without passing all tests.
  3. Infrastructure as Code (IaC): For more complex DigitalOcean setups (like managed Kubernetes clusters, databases, or firewalls), consider Terraform. It defines your infrastructure programmatically, ensuring consistency and reproducibility.
  4. Observability: Integrate robust logging (e.g., Winston, Pino), monitoring (e.g., DigitalOcean's built-in metrics, Prometheus/Grafana), and tracing (e.g., OpenTelemetry) to understand application health and performance in production.
  5. Security First: Implement security best practices from the start: input validation, authentication/authorization, secure cookie handling, dependency scanning, and regular security audits. DigitalOcean's firewall rules and VPC networks can segment your infrastructure.
  6. Environment Management: Use environment variables for configuration (process.env in Node.js, Vite's import.meta.env for frontend) and manage them securely in DigitalOcean App Platform or Kubernetes secrets.

Conclusion

Integrating Vue.js, Koa, and DigitalOcean with a strong foundation in TypeScript and modern tooling provides a powerful, scalable, and maintainable full-stack development experience. By adopting a monorepo, comprehensive testing, automated CI/CD, and cloud-native deployment strategies, teams can efficiently build and deliver high-quality applications like "Project Horizon" that meet the demands of today's dynamic web landscape. The synergy between these technologies empowers developers to focus on innovation, confident in their robust and well-orchestrated ecosystem.