teispace/nextjs-starter
Production-ready Next.js template.
The reference template behind the next-maker CLI. TypeScript, Redux Toolkit, next-intl, Tailwind, and a full quality-gate pipeline shipped on day one.
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 orderThese 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 tosrc/*. - Tailwind v4 with theme tokens declared in CSS (
@themeblock), notailwind.config.js. Dark + light mapped to the same token names sobg-bgworks 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.tsxexportsrenderWithProvidersfor components that need Redux + i18n. - Pino logger at
@/lib/loggerwith auto-redaction of common sensitive keys. Nothing in the template importsconsole.*. - 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.tsexportsuseAppDispatch/useAppSelector. - next-intl for i18n with locale-prefix
never. The CLI'sremove i18nknows how to peel this back if you don't need it. - husky with
pre-commitrunningenv:sync + lint-staged + type-check,pre-pushrunningci:check + type-check + check:deprecated + tests. - commitlint with conventional-commits, gated by a
commit-msghook. Useyarn commitfor 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 typesThe 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.tswithproxy.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
@themetokens, so a dark/light palette swap is a single CSS file change. - Added a
SmoothScrollProvider(Lenis) at the root, with properoverflow-x: clipinstead ofoverflow-x: hidden(the latter breaksposition: 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 installThen 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.