How to Do Web Development in 2026
The gap between junior and senior was never syntax: it's practices. The fastest way to absorb them is to start from a codebase small enough to read.
Here's the uncomfortable part about getting good at web development in 2026: it was never about knowing more syntax. A model will write you a route handler in seconds. What separates a hobby project from production software is practices: the quiet decisions about structure, types, boundaries, and automation that you normally learn only by making the mistakes.
There's a shortcut, though. You can absorb practices by reading code that already got them right.
The catch is that most starters are unreadable. Three hundred files, a dozen abstractions, a framework wrapped in a framework. You clone it, you can't find where anything lives, and you learn nothing except how to be afraid of it. A codebase you can't read can't teach you.
So the practice underneath every other practice is this: keep it small enough to read. That's the whole idea behind ZeroStarter. It isn't trying to be the biggest starter. It's trying to be the one you can actually finish reading: two apps, four shared packages, one type that runs from Postgres to the DOM, and no config files you have to reverse-engineer.
Read the whole thing, then steal these
Everything below is visible in the repo in one sitting. Don't take the specific libraries as gospel; take the practice.
One type across the wire
The contract between a frontend and a backend should be checked by the compiler, not by your users. ZeroStarter uses Hono RPC: the backend exports one AppType, and the client infers every route, input, and response from it: no codegen, no schema to keep in sync.
const { data, error } = await unwrap(apiClient.v1.session.$get())Rename a route or change a payload on the backend and this line stops compiling. The mismatch is a red squiggle in your editor, not a 500 in production. Whether you infer types (Hono RPC, tRPC) or generate them (GraphQL, OpenAPI), the practice is the same: make the network boundary type-safe.
One place for everything shared
Duplication is where bugs hide. Fix auth in one file and forget the other two, and you've shipped a vulnerability. A monorepo gives every shared concern exactly one home: the database schema lives once in packages/db, the auth instance in packages/auth, and the brand (name, description, links) in a single site.ts, so rebranding a fork is a one-file change. Change something, and Turborepo rebuilds only what depends on it.
The practice is clear boundaries and one source of truth. Monorepo, polyrepo, published packages. Pick your mechanism; just don't copy-paste the important logic.
Fail at the boundary, not in production
process.env.POSTGRES_URL is string | undefined, and "undefined" tends to reveal itself five minutes into a request in production. ZeroStarter validates environment variables at startup with Zod, typed per consumer so the frontend physically cannot read a server secret. The API uses the same instinct everywhere: one response envelope ({ data } on success, { error: { code, message } } on failure), and every route just throws. A single handler shapes the rest. Git hooks run the formatter, linter, and build before a commit ever lands.
The practice is catch it at the edge. A missing variable should crash the app immediately with a clear message, not corrupt a request an hour later.
Generate what you'd otherwise forget to update
Hand-maintained docs rot the moment code changes. So don't maintain them. ZeroStarter derives its API reference from the routes (Scalar at /api/docs), its sitemap and social images from its content, its changelog from commit messages, and its llms.txt from the docs. Each of these is the code, rendered, so it can't lie about what the code does.
The practice is single-source everything that drifts. If a fact lives in two places, one of them is already wrong.
The same legibility is what agents need
Here's the part that's new in 2026. A codebase small enough for a junior to read is also the codebase an AI agent can build in without breaking. That isn't a coincidence: it's the same property. Strict types give an agent guardrails; a small surface gives it less to misread; one obvious place per concern means it doesn't have to guess.
ZeroStarter leans into it with concrete affordances: executable skills that encode the repo's conventions, a one-request local sign-in so an agent can test behind auth, and generated context at /llms.txt. But none of that would matter on top of a sprawling, untyped codebase. The affordances work because the foundation is legible. Point Claude Code or Cursor at it and it ships a real feature: typed, so a wrong call fails at compile time.
Start reading
bunx zerostarter init
bun run dev # named .localhost URLsOne command scaffolds a fresh product, installs, provisions a local Postgres, and writes your .env. Then open the code. The practices in this post aren't described in a wiki somewhere; they're in the files, small enough to read end to end in an afternoon.
The specific choices are yours to change. The practices are worth keeping.
- Documentation: zerostarter.dev/docs
- AI-ready docs: zerostarter.dev/llms.txt
- GitHub: github.com/nrjdalal/zerostarter
MIT licensed. Read it, fork it, make it yours.