My Development Stack
After years of experimenting with different technologies, I've settled on a stack that keeps me productive and lets me ship quality applications quickly. Here's what I'm using these days and why each tool earned its place in my workflow.
Framework: Next.js + React + TypeScript
I've been building with React since 2016 and Next.js since 2019, so this is where I feel most at home. Every new project starts with TypeScript because catching errors at compile time beats debugging in production.
My approach to data fetching has evolved with React's newer patterns:
Most of the time: Server Components
I fetch data directly in Server Components whenever possible. It's simpler, faster, and eliminates the loading states that used to complicate client-side fetching.
export default async function BlogPage() {
const response = await fetch('https://api.example.com/posts')
const posts = await response.json()
return (
<div className="space-y-6">
{posts.map((post) => (
<article key={post.id} className="rounded-lg border p-4">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600">{post.excerpt}</p>
</article>
))}
</div>
)
}
For existing codebases or when I need client-side fetching, I still reach for SWR occasionally.
Sometimes: React's use
hook with Context
For complex apps where I need to share data across many components, I've started using React's use
hook with Context providers:
import { createContext, useContext } from 'react'
const DataContext = createContext(null)
export function DataProvider({ children }) {
const dataPromise = fetch('/api/data').then((res) => res.json())
return (
<DataContext.Provider value={{ dataPromise }}>
{children}
</DataContext.Provider>
)
}
export function useData() {
const context = useContext(DataContext)
if (!context) {
throw new Error('useData must be used within DataProvider')
}
return context
}
Then in any client component:
'use client'
import { use } from 'react'
import { useData } from './context'
export function DataDisplay() {
const { dataPromise } = useData()
const data = use(dataPromise)
return <div>{/* render data */}</div>
}
This pattern works great when you have global data that needs to be accessed deep in your component tree.
For forms, I use React 19's Server Actions with useActionState
, plus Zod for validation. It's a game-changer for handling form submissions without the traditional API route complexity.
Styling: Tailwind CSS + shadcn/ui
Building accessible, beautiful components from scratch is time-consuming. That's why I use shadcn/ui—it gives me professionally designed components built on accessible primitives.
The components are styled with Tailwind CSS, which has become my favorite way to write styles. Why? Because it's incredibly AI-friendly and keeps styles close to markup.
Why I love Tailwind
Tailwind's compiler only includes the classes you actually use, so your CSS bundle stays small no matter how many utility classes exist. You get predictable performance with a fixed upper bound on CSS size.
The real magic happens when working with AI coding assistants. Since styles are colocated with markup, AI tools can easily understand and modify both structure and styling in one go.
You're not locked into only Tailwind either—it plays nicely with CSS Modules, styled-components, or any other styling approach when you need them.
Database: PostgreSQL + Drizzle ORM
PostgreSQL is my database of choice for its reliability and feature set. Drizzle ORM makes working with Postgres enjoyable with full TypeScript support and excellent developer experience.
I love that I can visualize and edit data with Drizzle Studio, run type-safe queries, and handle migrations seamlessly with Drizzle Kit. The TypeScript integration is so good that database errors often get caught at compile time.
AI Assistant: v0 and Claude
v0 has become invaluable for code generation, especially when prototyping UI components or handling tedious refactoring tasks. Since it understands modern Next.js, React, and the tools I use, the code suggestions are usually spot-on.
I also use Claude for debugging complex issues, explaining unfamiliar codebases, and brainstorming architectural decisions. AI tools aren't perfect, but they've dramatically improved my productivity when used thoughtfully.
Development Philosophy
Over the years, I've developed some principles that guide how I write code:
Simplicity over cleverness: I prefer let
over const
in most cases because it's more flexible and the difference rarely matters in practice.
Consolidation over fragmentation: I'd rather have one larger, cohesive file than many tiny components scattered across directories. Related code should live close together.
Pragmatism over purity: Copy-pasting code is often better than creating the wrong abstraction. I optimize for readability and maintainability over theoretical elegance.
Performance awareness: I avoid putting SVGs directly in JSX for large applications, preferring SVG sprites for better performance and cacheability.
The Result
This stack lets me move fast without sacrificing quality. The tools work well together, have great TypeScript support, and don't get in my way when I need to ship features quickly.
Most importantly, everything is learnable and well-documented. When I need to onboard someone new or revisit old code, these tools make it straightforward.
Curious about any of these tools? Feel free to reach out if you have questions about implementing any part of this stack.