As an independent developer with over two years of experience in the freelance wilderness, I've learned that the tech stack you choose can make or break your project—and sometimes your sanity. Today, I want to share why I built Apex Kit, my full-stack monorepo template that has become my secret weapon for client projects.

The Client Scenario

Picture this: I landed a gig with a client who needed a solution for a specific problem. The catch? They specifically requested Vue because they wanted to use technology they understood. Fair enough—client comfort is important!

The project had a clear division of responsibilities:

  • I handled the frontend (my territory)
  • The client team developed the backend (their domain)
  • We connected through an API (our meeting point)

This is where things got interesting. As the TypeScript enthusiast I am, I found myself defining types for everything in the API. And every time the backend changed (which was... often), guess who had to update their code? Yep, yours truly.

I needed a better way to handle this dance between frontend and backend. That's when I decided to create a stack specifically designed for this scenario—and honestly, for my own peace of mind.

Why Vue?

When the client requested Vue, I was actually pretty happy about it. Vue has always been a solid choice for several reasons:

  1. Client familiarity: The team already understood Vue, which meant easier code reviews and knowledge sharing.
  2. The right tool for the job: Vue 3's Composition API provided exactly the flexibility and performance I needed.
  3. Lightweight and adaptable: Unlike some heavier frameworks, Vue doesn't force unnecessary structure on smaller projects.

But the most important factor? The client understood it. When working as a freelancer, it's not just about using the coolest tech—it's about creating solutions that clients can maintain long after your contract ends.

Why Not Nuxt?

"But wait," you might ask, "why not use Nuxt? Isn't it the go-to meta-framework for Vue?"

That's a valid question! Nuxt is awesome, and I've used it on other projects. But for this particular case, I made a conscious decision to skip it for a few reasons:

  1. Overhead vs. Need: Nuxt adds a layer of complexity that wasn't necessary for what we were building. Why add extra weight when a simpler solution would do?

  2. Control and Flexibility: I wanted granular control over the project structure without being locked into Nuxt's conventions. Sometimes you just need to organize things your way!

  3. Backend Integration: Since we were building a custom API integration with clear type definitions, many of Nuxt's advantages in data fetching and server-side logic weren't as relevant.

  4. Build Performance: With Vite directly, builds were lightning-fast without the additional abstraction layer.

Don't get me wrong—Nuxt is fantastic for many projects! But as a freelancer, I've learned to be pragmatic about choosing the right tool for each specific job rather than defaulting to the full-featured option.

The Monorepo Approach with Turborepo

This is where things get interesting! I decided to structure the project as a monorepo using Turborepo. If you haven't explored monorepos yet, they're a game-changer for projects with interconnected pieces.

Here's why this approach was a lifesaver:

Shared Types Across Frontend and Backend

The key insight was realizing I could keep all API types in a tRPC client/server that both frontend and backend reference. This meant:

apex-kit/
├── apps/
│   ├── backend/        # Hono API running on Cloudflare Workers
│   └── frontend/       # Vue 3 application
└── packages/
    └── .../            # Common configs and utilities

This structure eliminated the constant back-and-forth of "the API changed, now I need to update my frontend types." With tRPC in the mix, type safety became automatic!

Easier Local Development

Turborepo made it incredibly simple to run both frontend and backend simultaneously with a single command:

pnpm run dev

This would kick off both services with proper watching of shared code. When I made changes to shared types, both frontend and backend would update automatically. And of course, it will show type errors.

Faster Builds and CI

Turborepo's caching is absurdly efficient. After the first build, subsequent builds only processed what changed—cutting build times dramatically. For a freelancer juggling multiple tasks, this time-saving is golden.

Why Vue + Hono + tRPC?

The beautiful trio that made this stack special:

Vue 3: Modern and Flexible

Vue 3 was already client-mandated, but I was thrilled about it. The Composition API made component logic cleaner and more reusable. Combined with TypeScript, it provided an excellent foundation for a maintainable frontend.

Hono: Lightning-Fast API on the Edge

For the backend, I needed something lightweight yet powerful. Hono running on Cloudflare Workers was the perfect fit:

  1. Performance: Running on the edge means incredibly low latency.
  2. Cost-effective: The generous free tier worked perfectly for this project.
  3. TypeScript-native: No awkward type integration—Hono embraces TypeScript from the start.

tRPC: The Magic Glue

Finally, the piece that tied everything together: tRPC. If you haven't used tRPC yet, it's a game-changer for type-safe APIs. Here's what made it invaluable:

  1. End-to-end type safety: Define procedures on the backend, and the frontend automatically knows their parameters and return types.
  2. No code generation: Unlike GraphQL, there's no need to run code generators when the API changes.
  3. Incredible developer experience: Auto-completion for API calls is magical.

Here's a quick example of how clean the code becomes:

// Backend procedure definition
const appRouter = apex({
  greeting: publicProcedure
    .input(
      z.object({
        name: z.string(),
      }),
    )
    .query(({ input }) => {
      return {
        message: `Hello, ${input.name}!`,
        timestamp: new Date(),
      };
    }),
});

// Frontend usage with full type inference
const result = await trpc.greeting.query({ name: "Client" });
console.log(result.value.message); // TypeScript knows this exists!

Need to handle complex data fetching? No problem! TanStack Query is a game-changer for data fetching in Vue.

<script setup lang="ts">
import { trpc } from "@/api/trpc";
import { useQuery } from "@tanstack/vue-query";

const {
  data: greeting,
  error,
  isLoading,
} = useQuery({
  queryFn: async () => {
    const result = await trpc.greeting.query({ name: "Client" });
    return result;
  },
  queryKey: ["greeting"],
});
script>

<template>
   v-if="isLoading">Loading post...
   v-else-if="error" class="text-red-500">Error loading greeting: {{ error }}
   v-else>{{ greeting?.message }} - {{ greeting?.timestamp }}
template>

Communication Between Frontend and Backend

This stack fundamentally changed how I communicated with the client's backend team. Instead of the constant cycle of:

  1. Backend team changes an endpoint
  2. They document the changes (hopefully)
  3. I update my TypeScript interfaces
  4. I discover missed edge cases during testing
  5. Repeat

We now had a much smoother workflow:

  1. We agree on tRPC procedure definitions in the shared codebase
  2. Backend team implements the logic
  3. Frontend (me) consumes the endpoints with guaranteed type safety

This approach caught countless potential bugs before they happened. When the API contract changed, TypeScript immediately flagged every place in the frontend that needed updates. No more runtime surprises!

The Result: Apex Kit

What started as a solution for one client project evolved into Apex Kit—my go-to template for freelance projects. It's designed specifically for the freelancer scenario where you need:

  1. Fast setup for new projects
  2. A clean division between frontend and backend responsibilities
  3. Type safety across the entire stack
  4. Great developer experience (because life's too short for bad DX)

The best part? This stack isn't just theoretical—it's battle-tested in real client work. It's saved me countless hours of debugging and made maintenance so much easier.

Lessons Learned

As a freelancer, this project reinforced some key principles:

  1. Prioritize communication interfaces: The boundary between your code and others' code needs special attention.
  2. Build for maintenance: Choose technologies that make ongoing updates easier.
  3. Optimize for your workflow: Create systems that solve your specific pain points.
  4. Don't default to the heaviest solution: Sometimes simpler is better!

The stack I built wasn't just about technical elegance—it was about creating a sustainable workflow that made my life as a freelancer easier while delivering excellent results for clients.

And isn't that what good technology choices should do?


If you're curious about trying this stack yourself, check out Apex Kit on GitHub. I'd love to hear your thoughts or questions in the comments!