krishna@
~/projects
liveopen source · 2024 →

teispace/nextjs-starter

Production-ready Next.js template.

teispace/nextjs-starter — preview
01 // overview

The reference template behind the next-maker CLI. TypeScript, Redux Toolkit, next-intl, Tailwind, and a full quality-gate pipeline shipped on day one.

02 // the.story

the relationship to next-maker

This repo and the next-maker CLI are the same thing in two forms.

The CLI publishes a snapshot of this repo as the scaffold output. Every commit here gets bundled into the next CLI publish via the build pipeline. So when someone runs npx @teispace/next-maker init my-app, what they get is today's version of this template, not whatever was committed when the CLI itself was last touched.

That makes this repo the source of truth. Everything I learn about a real Next.js setup goes here first, then propagates.

the pipeline is the product

Most Next.js starters ship a happy-path yarn dev and call it done. The interesting question — will this fail loudly when something drifts? — is left as an exercise. This template's pitch is that the answer is yes.

The quality gates:

yarn ci:check        # Biome — lint + format + import sort
yarn type-check      # tsc --noEmit
yarn check:deprecated # custom — catches @deprecated usage
yarn test            # Vitest
yarn build           # Next.js production build
yarn validate        # all of the above, in order

These run in three places: as you save files (Biome via your editor extension), on git push (husky pre-push), and in CI on every PR. Same gates everywhere, no "works on my machine."

why check:deprecated is a custom step

TypeScript has @deprecated JSDoc annotations. When you use a deprecated API, your editor crosses it out and tsc surfaces it — but only as a suggestion, the lowest severity tier. tsc --noEmit exits 0 with deprecation suggestions on stderr. CI passes. You ship deprecated code.

The starter ships a small script that uses the TypeScript compiler API directly:

const program = ts.createProgram(...);
const diagnostics = ts.getPreEmitDiagnostics(program);
const deprecations = diagnostics.filter(
  (d) => d.reportsDeprecated || d.code === 6385,
);
if (deprecations.length) process.exit(1);

It walks the project, pulls suggestions tagged "deprecated," and exits non-zero if any exist. CI fails. You don't ship deprecated code.

This catches things tsc won't: a peer dependency upgrades, deprecates an API your code uses, and the only signal is an editor strikethrough that nobody notices. check:deprecated makes it a build break.

what's wired

  • Strict TypeScript, ESM-only, paths set up so @/* resolves to src/*.
  • Tailwind v4 with theme tokens declared in CSS (@theme block), no tailwind.config.js. Dark + light mapped to the same token names so bg-bg works in both themes.
  • Biome configured to be opinionated where it should be (single quotes, no semicolons before destructuring) and quiet where it shouldn't (no preferential ordering of class names — Tailwind class sorting is a separate, optional rule).
  • Vitest + React Testing Library + jsdom. test/test-utils.tsx exports renderWithProviders for components that need Redux + i18n.
  • Pino logger at @/lib/logger with auto-redaction of common sensitive keys. Nothing in the template imports console.*.
  • Zod env validation at @/lib/env. Imports that crash at module load fail fast — better than discovering a missing env var in production.
  • Redux Toolkit + redux-persist with typed hooks. The store is assembled in src/store/; hooks.ts exports useAppDispatch / useAppSelector.
  • next-intl for i18n with locale-prefix never. The CLI's remove i18n knows how to peel this back if you don't need it.
  • husky with pre-commit running env:sync + lint-staged + type-check, pre-push running ci:check + type-check + check:deprecated + tests.
  • commitlint with conventional-commits, gated by a commit-msg hook. Use yarn commit for a guided prompt.
  • GitHub Actions workflow that mirrors the husky pre-push pipeline, plus the production build (which husky deliberately doesn't run — that's CI's job).

the directory shape

src/
  app/           App Router routes
  app/[locale]/  Localized routes (i18n; remove if not needed)
  components/    Cross-feature shared components
  features/      Feature folders — components/, hooks/, store/, types/
  i18n/          next-intl config
  lib/           Config, enums, errors, utils, validations
  providers/     Root + nested React providers
  proxy.ts       Edge proxy (Next 16's middleware.ts replacement)
  services/      api/, storage/
  store/         Redux store, persistor, hooks, rootReducer
  styles/        Global CSS
  types/         Cross-cutting TS types

The features/ folder is the most opinionated choice. New domain logic goes in features/<feature>/, not scattered across components/ and store/. This makes deletion a one-folder operation rather than a search-and-replace.

what I keep changing

Each time I start a new project, I notice one or two things in this template that bother me. Those become commits, and via the CLI, they propagate to the next person who scaffolds. The most recent batch:

  • Replaced middleware.ts with proxy.ts (Next 16's new edge interception primitive). The old middleware API still works but the runtime is being deprecated.
  • Migrated all CSS color references from Tailwind utilities to @theme tokens, so a dark/light palette swap is a single CSS file change.
  • Added a SmoothScrollProvider (Lenis) at the root, with proper overflow-x: clip instead of overflow-x: hidden (the latter breaks position: sticky).

Each of these took a real project to surface. The template is where the lesson gets recorded.

using it directly

If you don't want the CLI — you just want the template as a starting point — clone the repo:

git clone https://github.com/teispace/nextjs-starter my-app
cd my-app && yarn install

Then strip the parts you don't need (the CLI's remove command does this automatically, but it's also straightforward by hand).

The CLI is the recommended path because of doctor. With the CLI, you can run npx @teispace/next-maker doctor later and learn what's drifted from current. Without the CLI, you fork the moment in time and you're on your own.