Building a Modern Full-Stack Web Application - Part 1
LLMs made it cheap to prototype frameworks and libraries. I used that to build a full-stack web application with modern TypeScript tooling. This is Part 1: tech stack and architecture. Part 2 covers deployment and workflow.
Tech Stack
The foundation is React Router v7 with TypeScript, PostgreSQL for data, and modern UI libraries:
Frontend: React Router v7 with React 19.1.1, TypeScript 5.9.2 for type safety
UI & Styling: Tailwind CSS v4.1.13, Catalyst UI components, Headless UI, Heroicons, Motion for animations
Backend & Data: Node.js v24+, PostgreSQL with Neon serverless, Drizzle ORM v0.44.5 for type-safe queries
Authentication: better-auth v1.3.9 with WebAuthn, OAuth, and session management. Zod v4.1.5 for validation
Dev Tools: Vite v7.1.5, ESLint, Prettier, Vitest, Playwright, Storybook
Architecture Decisions
Type-Safe Data Layer
Using Drizzle ORM provides full type safety across the entire data layer:
// 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();
The database schema uses normalized tables with foreign keys, JSON columns for flexible data, and indexes on frequently queried columns.
Authentication with Multiple Methods
The authentication layer uses better-auth: email/password, WebAuthn/passkeys, OAuth, and HTTP-only cookie sessions.
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 React Router v7 Typegen
React Router v7's typegen automatically generates route-specific types based on your file 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' },
];
};
export async function loader({ params, request }: Route.LoaderArgs) {
const user = await requireAuth(request);
const data = await getData(params.id);
return { data, user };
}
export default function Component() {
const { data, user } = useLoaderData<typeof loader>();
return <div>...</div>;
}
Tip
This pattern eliminates separate API routes. Type safety extends from loader arguments to meta functions to client-side data consumption.
Why This Stack Works
Vite gives fast HMR and automatic code splitting. Drizzle ORM handles prepared statements and connection pooling. Better-auth removes authentication boilerplate. React Router v7's typegen catches bugs at compile time instead of runtime.
TypeScript caught dozens of potential runtime errors during development. That alone justified the stack choice.
Continue to Part 2 for deployment, workflows, and performance.