Hey there, toolkit enthusiasts and fellow digital operatives! Riley Fox here, back at agntkit.net with another dive into the stuff that makes our lives easier, more efficient, and frankly, a lot more fun. Today, I want to talk about something that’s been a bit of a quiet hero in my own workflow lately: the ‘starter’ project. Not just any starter, mind you, but the kind of meticulously crafted, opinionated starter that dramatically cuts down on setup time and lets you jump straight into the good stuff.
For a long time, I was a bit of a purist. “Build it from scratch,” I’d tell myself. “You’ll learn more.” And you know what? Sometimes that’s true. You absolutely learn the intricacies of a framework or a language when you’re hand-rolling every config file, every dependency. But let’s be honest, how many times do you need to learn how to set up Webpack again? Or configure ESLint for the hundredth time? Or wrestle with database migrations on a new project?
My epiphany came a few months ago when I was tasked with spinning up a quick prototype for a client. It wasn’t a huge project, but it needed a backend API, a simple frontend, authentication, and a database. My usual M.O. would have involved spending a good day, maybe two, just getting the foundational pieces talking to each other. But this time, I was under a tight deadline, and I remembered a conversation with a developer friend who swore by his “personal starter kit.”
The Underrated Power of a Good Starter
What exactly am I talking about when I say ‘starter’? I’m not talking about a boilerplate that just gives you a `hello world`. I’m talking about a comprehensive, pre-configured project template that includes your preferred technologies, best practices, and often, some common utilities already wired up. Think of it as your ideal development environment, but already built and ready for your specific project ideas.
My friend’s starter, for example, was a Next.js frontend with a tRPC backend, connected to a Postgres database, complete with Prisma ORM and Clerk for authentication. He had spent weeks refining it, adding all the little quality-of-life improvements he always found himself implementing in every new project. And when I saw him spin up a new project from it in literally minutes, I was floored.
It was like he had a magic wand. He wasn’t just copying files; he was cloning a Git repo, running an install script, and boom – a fully functional, albeit empty, application stack was ready. No more Googling “Next.js Prisma setup,” no more fighting with authentication flows. The core plumbing was done.
Beyond Boilerplates: Why Starters Are Different
You might be thinking, “Riley, isn’t that just a boilerplate?” And while there’s overlap, I see a key difference. A boilerplate often provides the bare minimum. A starter, in my experience, is more opinionated and more complete. It reflects a specific philosophy or workflow. It’s not just about getting code to compile; it’s about getting to a state where you can immediately start building features.
For instance, a simple React boilerplate might give you `create-react-app`. A React *starter* might give you `create-react-app` plus:
- Pre-configured Tailwind CSS
- ESLint and Prettier rules tailored to a specific style guide
- Storybook setup for component development
- A basic routing structure using React Router DOM
- Maybe even a pre-built authentication context or a dark mode toggle.
It’s the difference between being handed a toolbox and being handed a fully assembled workbench with your favorite tools already laid out.
My Journey to Building My Own Starter
After seeing my friend’s setup, I was inspired. My own workflow often involves a Node.js API (Express or Fastify) and a vanilla JavaScript frontend, sometimes with Web Components. The setup invariably includes:
- TypeScript for both backend and frontend.
- ESLint and Prettier.
- A database (usually SQLite for quick prototypes, Postgres for anything serious) with Knex.js for migrations and query building.
- Basic user authentication (JWT-based).
- CORS configuration.
- A simple build process for the frontend (Rollup or Vite).
- Docker Compose for easy local development.
Each time, it was like starting from square one. Copying files from old projects, fixing broken dependencies, re-learning configs. It was death by a thousand papercuts. So, I decided to invest the time.
My goal wasn’t just to save time, but to enforce consistency. When I’m working with a team, or even just myself across multiple projects, having a consistent base makes context switching so much smoother. You know where things are, how they’re configured, and what to expect.
Anatomy of My Go-To Starter: ‘AgentKit Base’
My starter, which I’ve lovingly dubbed ‘AgentKit Base’, is a monorepo setup using pnpm workspaces. It contains two main packages:
api: A Fastify server with TypeScript, Knex.js, SQLite (default, easily swapped for Postgres), JWT authentication, and basic user management routes.client: A vanilla TypeScript frontend using Vite for bundling, SCSS for styling, and a barebones routing system.
Here’s a peek at how the api/src/routes/auth.ts looks for a login endpoint:
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import db from '../db'; // Knex instance
import { User } from '../types'; // Basic User interface
interface LoginBody {
email: string;
password: string;
}
export default async function authRoutes(fastify: FastifyInstance) {
fastify.post('/login', async (request: FastifyRequest<{ Body: LoginBody }>, reply: FastifyReply) => {
const { email, password } = request.body;
const user: User | undefined = await db('users').where({ email }).first();
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return reply.status(401).send({ message: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, { expiresIn: '1h' });
reply.send({ token });
});
// Example of a protected route
fastify.get('/profile', { preHandler: fastify.auth }, async (request: FastifyRequest, reply: FastifyReply) => {
// @ts-ignore - userId added by auth preHandler
const userId = request.userId;
const user: User | undefined = await db('users').where({ id: userId }).first();
if (!user) {
return reply.status(404).send({ message: 'User not found' });
}
// Omit sensitive data
const { passwordHash, ...safeUser } = user;
reply.send(safeUser);
});
}
And on the client side, a simple fetch to this endpoint:
// client/src/api.ts
async function login(email: string, password: string): Promise<string | null> {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Login failed');
}
const data = await response.json();
localStorage.setItem('authToken', data.token);
return data.token;
} catch (error) {
console.error('Login error:', error);
return null;
}
}
The beauty of this is that the authentication flow, the database setup, and the basic API structure are all there. I just clone the repo, run pnpm install, pnpm dev, and I have a working full-stack application ready to extend. I can then focus on building the unique features for the specific project, rather than reinventing the wheel.
When to Build vs. When to Use a Starter
This isn’t to say you should *always* use a starter. There are definitely times when starting fresh is the right call:
- Learning a new technology: If you’re genuinely trying to understand how a framework or tool works from the ground up, build it yourself.
- Highly custom requirements: If your project has extremely specific, unusual architectural needs that deviate significantly from common patterns, a starter might be more of a hindrance than a help.
- Experimentation: Sometimes you just want to play around without the overhead of a full starter.
But for most production-oriented work, or even complex prototypes, a well-chosen or well-built starter is a massive time-saver. It’s like having a personal assistant who sets up your workspace exactly how you like it every morning.
Actionable Takeaways for Your Own Workflow
Alright, so how can you apply this to your own agent toolkit?
-
Identify Repetitive Setup Tasks
Look at your last 3-5 projects. What were the first things you always set up? What configurations did you find yourself copying and pasting? What dependencies were always there? These are prime candidates for your starter.
-
Choose Your Core Stack
Don’t try to build a starter for every possible technology combination. Pick your go-to frontend framework, backend language, database, and authentication method. The more opinionated it is, the more useful it will be for *your* specific needs.
-
Start Small, Iterate
You don’t need to build a perfect starter overnight. Start with the absolute essentials. Get a basic “hello world” working with your preferred stack. Then, as you encounter repetitive tasks in future projects, add them to your starter. It’s a living document, a growing asset.
-
Document Everything
Even if it’s just for yourself, document how to use your starter, what each part does, and any common configuration changes. Future You will thank Past You.
-
Consider Monorepos
If you often build full-stack applications, a monorepo setup (like pnpm workspaces or Lerna) can be incredibly powerful for managing your frontend and backend in one place.
-
Share (Carefully)
If you work in a team, a shared starter can standardize development environments and significantly boost team productivity. Just make sure everyone agrees on the conventions.
Investing time in a good starter project is like investing in a high-quality tool for your workshop. You might spend a bit upfront, but the dividends in saved time, reduced frustration, and increased consistency will pay off exponentially. Stop setting up the same things over and over again. Build your ultimate ‘starter’ and get straight to building the cool stuff!
Until next time, keep building smarter, not harder!
🕒 Published: