Frontend stacks¶
| Stack | Language | What it is |
|---|---|---|
react-vite |
JavaScript | React 19 + Vite — react-router, an API client, a dev proxy |
react-vite-ts |
TypeScript | The same project, typed |
The two are deliberately the same project twice. Same folders, same components, same router — the TypeScript one adds types and changes nothing else. That is what makes "should this be TypeScript" a decision you make at scaffold time rather than one you have to live with.
Choosing a look¶
--styling is to a frontend what --database
is to a backend: the template ships every variant and the scaffold keeps exactly
one.
lattice shop --stack react-vite --styling tailwind
lattice shop --stack react-vite-ts --styling bootstrap
| Adds | Touches vite.config.js |
|
|---|---|---|
plain (default) |
nothing | no |
scss |
sass (dev) |
no |
bootstrap |
bootstrap, sass (dev) |
no |
tailwind |
tailwindcss, @tailwindcss/vite (dev) |
yes |
What makes the four interchangeable is a class-name contract:
.app__header · .app__brand · .app__nav · .app__main · .table ·
.pager · .muted · .error
The components are written against those names and never against a framework's, so choosing Bootstrap changes the stylesheet and nothing else. Plain and SCSS compile to byte-identical CSS; Bootstrap and Tailwind are themed to match the others rather than left at their defaults.
The variants you did not choose are deleted, not left unimported — a stray
bootstrap.scss stops compiling the moment anyone imports it, because Bootstrap
is only installed if it was chosen. What ships is one src/styles.css (or
styles.scss), so the project reads as though it had only ever had one look.
Sass and Tailwind land in devDependencies. They are compile-time tools, and a
CSS compiler has no business in a production install.
Only Tailwind touches the build
The other three leave vite.config.js reading exactly as though the feature
did not exist. That is worth more than it sounds: a scaffolder that adds
machinery for a choice you did not make is a scaffolder whose output you have
to read before you can trust.
Layout¶
src/
lib/api.js the single place that knows how to talk to the backend
hooks/useUsers.js data fetching, one hook per resource
pages/ one component per route
layouts/ RootLayout — the shell every page renders inside
components/ the pieces used by more than one page
router.jsx react-router configuration
main.jsx
Talking to a backend¶
src/lib/api.js is the only file that knows where the API lives, and the only
one that calls fetch. It understands the backends'
shared error envelope and turns it into a thrown
ApiError carrying the status, the message and the validation details:
try {
await api.post('/users', { email, password });
} catch (error) {
// error.status, error.message, error.details — from the envelope
}
That is the part that is easy to skip and expensive to skip. fetch does not
reject on a 4xx, so a component that awaits a bare fetch treats a rejected
write as a success. Every request in the generated project goes through the one
function that checks.
No CORS, no hardcoded URL¶
In development, Vite proxies /api to the backend, so the browser only ever
talks to one origin:
The base URL stays relative, so nothing absolute is compiled into the bundle. For
production, where the API may live on another origin, VITE_API_URL overrides
it — and it is the only variable in .env.example.
When you scaffold a fullstack project, the port that proxy points at is the port the backend was actually given, and the dev server takes a different one, so both halves run at once without a collision to discover.
What CI proves about these¶
templates-javascript.yml scaffolds both frontends and runs every check the
template declares — not a check chosen in the workflow. The list is derived
from the generated project's own package.json, so a script added to a template
is picked up automatically instead of being written down in two places and
drifting.
In practice that is lint, build and prettier --check for both, plus
typecheck for the TypeScript one. Each styling variant is scaffolded and built
separately on top of that, and its compiled CSS grepped for the class
contract — a Tailwind run that matched nothing would still produce a stylesheet
and still go green otherwise. All of those had existed in the templates
long before anything ran them: the workflow used to run npm run build for
react-vite and nothing else, the TypeScript template's typecheck had never
run at all, and the first prettier --check against a scaffolded project found
23 unformatted files across five templates. None of it was broken. None of it was
verified.