KahWee

Thoughts on web development, programming, and technology

Building a Modern Full-Stack Web Application

I'm currently building a full-stack web application using modern JavaScript/TypeScript technologies. The project is still in active development, but I've established a pretty good foundation with a focus on performance, developer experience, and scalable architecture. LLM has made it really easy for me to prototype frameworks and libraries very easily with relatively low costs. Here's kinda the stack I ended up with.

Tech Stack

Frontend Framework

  • React Router v7 - The latest version with built-in server-side rendering, data loading, and routing
  • React 19.1.1 - Latest stable React with concurrent features and improved performance
  • TypeScript 5.9.2 - Full type safety throughout the application

UI & Styling

  • Tailwind CSS v4.1.13 - Utility-first CSS framework with modern features
  • Tailwind Plus - Premium Tailwind CSS components and patterns
  • Catalyst UI - High-quality React components built on Tailwind CSS
  • @headlessui/react v2.2.7 - Unstyled, accessible UI components
  • @heroicons/react v2.2.0 - Beautiful, consistent icon set
  • motion v12.23.12 - Animation library for smooth interactions

Backend & Data

  • Node.js v24+ - Latest Node.js with modern JavaScript features
  • PostgreSQL - Robust, ACID-compliant relational database
  • drizzle-orm v0.44.5 - Type-safe SQL query builder and migration tool
  • Neon - Serverless PostgreSQL platform

Authentication & Security

  • better-auth v1.3.9 - Modern authentication library with WebAuthn, OAuth, and session management
  • zod v4.1.5 - Runtime type validation and schema validation

Development & Build Tools

  • vite v7.1.5 - Fast build tool and development server
  • eslint v9.35.0 - Code linting and quality enforcement
  • prettier v3.6.2 - Code formatting
  • vitest v3.2.4 - Fast unit testing framework
  • playwright v1.55.0 - End-to-end testing
  • storybook v9.1.5 - Component development and documentation

Architecture Overview

Application Structure

The application follows a modern full-stack architecture with clear separation of concerns:

├── app/                    # Main application code
│   ├── components/         # Reusable UI components
│   ├── routes/            # Route handlers and pages
│   ├── models/            # Data access layer
│   ├── db/               # Database schema and connections
│   ├── lib/              # Shared utilities and services
│   ├── utils/            # Helper functions
│   └── hooks/            # Custom React hooks
├── public/               # Static assets
├── migrations/          # Database migration files
└── test-utils/          # Testing utilities

Data Layer Architecture

Database Schema Design

The database schema is designed with performance and maintainability in mind:

  • Normalized structure with proper foreign key relationships
  • JSON columns for flexible data storage where needed
  • Indexes on frequently queried columns
  • Unique constraints to maintain data integrity
  • Audit fields (createdAt, updatedAt) on all tables

ORM and Query Layer

Using drizzle-orm provides several advantages over traditional ORMs:

// Type-safe queries with full IntelliSense
const users = await db.query.users.findMany({
  where: eq(users.isActive, true),
  with: {
    profile: true,
    settings: true,
  },
});

// Prepared statements for performance
const getUserById = db
  .select()
  .from(users)
  .where(eq(users.id, sql.placeholder('id')))
  .prepare();

const user = await getUserById.execute({ id: userId });

The type safety extends to the entire data layer, catching potential issues at compile time rather than runtime.

Authentication System

The authentication layer uses better-auth with multiple authentication methods:

  • Email/Password - Traditional authentication with secure password handling
  • WebAuthn/Passkeys - Passwordless authentication for enhanced security
  • OAuth - Social login integration (Google, GitHub, etc.)
  • Session Management - Secure HTTP-only cookies with CSRF protection
  • MCP Integration - Model Context Protocol support for AI assistants
// Simple auth setup with multiple providers
export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: "pg",
  }),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
  passkey: {
    enabled: true,
  },
});

API Architecture with Type Generation

The application uses React Router's built-in data loading patterns with react-router typegen for full type safety:

// Auto-generated types from route structure
import type { Route } from './+types/app.dashboard._index';

export const meta: Route.MetaFunction = () => {
  return [
    { title: 'Dashboard - My App' },
    { name: 'description', content: 'Application dashboard overview' },
  ];
};

// Server-side data loading with typed arguments
export async function loader({ params, request }: Route.LoaderArgs) {
  const user = await requireAuth(request);
  const data = await getData(params.id);

  return { data, user };
}

// Client-side data consumption with typed loader data
export default function Component() {
  const { data, user } = useLoaderData<typeof loader>();

  // Data is available immediately on page load with full type safety
  return <div>...</div>;
}

React Router v7's typegen automatically generates route-specific types based on your file structure, providing:

  • Type-safe loader arguments - params, request, and context are properly typed
  • Automatic meta function types - SEO and metadata with IntelliSense
  • Loader data inference - useLoaderData knows exactly what data is available
  • Action type safety - Form submissions and mutations are fully typed

This pattern eliminates the need for separate API routes and provides better performance through server-side rendering, all while maintaining complete type safety.

Form Handling

Forms are built using @tanstack/react-form with zod validation:

// Type-safe form with validation
const form = useAppForm({
  defaultValues: { email: '', password: '' },
  validators: {
    onChange: ({ value }) => {
      const errors = {};
      if (!value.email) errors.email = 'Email is required';
      if (!value.password) errors.password = 'Password is required';
      return { fields: errors };
    },
  },
  onSubmit: async ({ value }) => {
    await submitForm(value);
  },
});

The integration with zod provides runtime type checking and automatic TypeScript inference.

Hosting & Deployment

Infrastructure Setup

Fly.io - Primary hosting platform

  • Global CDN - Automatic content delivery worldwide
  • Auto-scaling - Scale to zero when not in use
  • Health checks - Automatic monitoring and restarts
  • Database migrations - Run migrations during deployment

Cloudflare - CDN and DNS

  • Global edge network - Fast content delivery
  • DDoS protection - Built-in security
  • SSL certificates - Automatic HTTPS

Database Hosting

Neon - Serverless PostgreSQL

  • Auto-scaling - Scale compute resources automatically
  • Branching - Database branching for development and staging
  • Backups - Automatic point-in-time recovery
  • Connection pooling - Efficient connection management

Deployment Configuration

The application is containerized using Docker with multi-stage builds:

FROM node:24-alpine AS base
WORKDIR /app

# Multi-stage build for optimization
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM base AS build
COPY . .
RUN npm run build

FROM base AS runtime
COPY --from=build /app/build ./build
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "start"]

CI/CD Pipeline

The deployment pipeline ensures code quality and reliability:

  • Automated testing on every push
  • Type checking and linting with eslint
  • Database migrations during deployment
  • Health checks post-deployment
  • Rollback capability for failed deployments

Development Workflow

Local Development

# Start development server
npm run dev

# Run tests
npm run test

# Type checking
npm run typecheck

# Code quality checks
npm run check

# Database operations
npm run db:studio    # Visual database management
npm run db:seed      # Populate with sample data
npm run db:migrate   # Apply schema changes

Code Quality

The project maintains high code quality standards:

  • eslint - Code linting with React and TypeScript rules
  • prettier - Consistent code formatting
  • TypeScript - Strict type checking with noUncheckedIndexedAccess
  • husky - Git hooks for pre-commit quality checks
  • vitest - Unit and integration testing
  • playwright - End-to-end testing

Database Development

Database development follows a structured approach with drizzle-kit:

# Schema changes
1. Edit app/db/schema.ts
2. Generate migration: npm run db:generate
3. Apply migration: npm run db:migrate

# Development workflow
npm run db:push     # Quick schema sync (development only)
npm run db:studio   # Visual database management
npm run db:reset    # Clean database reset

The drizzle-kit studio provides a web-based interface for database inspection and debugging.

Component Architecture

UI Components with Catalyst UI

The application uses Catalyst UI components built on top of Tailwind CSS and @headlessui/react:

import { Button } from '@catalyst-ui/react'
import { PlusIcon } from '@heroicons/react/16/solid'

export function CreateButton() {
  return (
    <Button color="indigo" className="gap-2">
      <PlusIcon className="size-4" />
      Create New
    </Button>
  )
}

Catalyst UI provides production-ready components with excellent accessibility and consistent design patterns.

Custom Hooks for State Management

The application uses custom hooks for complex state logic:

// Custom hook for data fetching with error handling
export function useAsyncData<T>(fetcher: () => Promise<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetcher()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { data, loading, error };
}

Performance Considerations

Build Optimization

Using vite provides several performance benefits:

  • Fast HMR - Sub-second hot module replacement
  • Tree shaking - Automatic dead code elimination
  • Code splitting - Automatic route-based splitting
  • Asset optimization - Image compression and modern formats

Database Performance

drizzle-orm offers performance advantages:

  • Prepared statements - Cached query plans
  • Connection pooling - Efficient database connections
  • Query optimization - Minimal over-fetching
  • Type-safe migrations - Prevent runtime errors

Lessons Learned

Building this application taught me several important lessons:

  1. Modern tooling matters - vite, drizzle-orm, and better-auth significantly improved the development experience
  2. Type safety is crucial - TypeScript caught numerous potential runtime errors
  3. Component libraries save time - Catalyst UI provided high-quality components without the overhead
  4. Infrastructure automation - Fly.io's deployment pipeline reduced operational complexity

This architecture demonstrates how modern JavaScript/TypeScript tooling can create maintainable, performant applications with excellent developer experience. The combination of React Router v7, drizzle-orm, better-auth, and Fly.io provides a powerful foundation for building full-stack applications in 2025.

All Tags