Lancer un projet TypeScript full-stack moderne implique généralement d'assembler à la main une demi-douzaine d'outils : un framework frontend, un serveur backend, une couche API, un ORM, l'authentification, le linting et un système de build pour le monorepo. Chacun a sa propre configuration, et les jointures entre eux sont précisément là où la sûreté de typage s'évapore.
Better-T-Stack supprime cette friction. C'est une CLI open source (create-better-t-stack) qui génère un monorepo typé de bout en bout où le schéma de votre base de données, les contrats d'API et le code frontend partagent un seul jeu de types. Modifiez une colonne dans votre schéma et TypeScript signale le composant frontend cassé avant même que vous ne lanciez l'application.
Dans ce tutoriel, vous allez générer une application complète — un backend Hono, une API tRPC, une couche de données Drizzle, Better Auth et un frontend TanStack Router — puis câbler une petite fonctionnalité de bout en bout pour prouver que la sûreté de typage est réelle.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Bun 1.1+ installé (
curl -fsSL https://bun.sh/install | bash) — Better-T-Stack utilise Bun comme runtime et gestionnaire de paquets par défaut. Node.js 20+ fonctionne également. - Des connaissances de base en TypeScript — les génériques et l'inférence apparaîtront.
- Une familiarité avec React — le frontend par défaut est React avec TanStack Router.
- Un éditeur de code doté du serveur de langage TypeScript (VS Code recommandé).
- Environ 30 minutes.
Ce que vous allez construire
À la fin, vous aurez un monorepo fonctionnel avec deux applications :
apps/web— un frontend React (TanStack Router + Tailwind + shadcn/ui)apps/server— un serveur Hono exposant une API tRPC adossée à Drizzle et SQLite
Vous ajouterez une table notes, exposerez une procédure create et une list typées, et les consommerez depuis un composant React — le tout sans écrire un seul endpoint REST ni un appel fetch typé à la main.
Étape 1 : générer le projet
Better-T-Stack fournit une seule commande. Vous pouvez l'exécuter en mode interactif et répondre aux invites, ou passer des drapeaux pour une configuration reproductible. Commençons en interactif pour voir chaque choix :
bun create better-t-stack@latestLa CLI vous guide à travers le frontend, le backend, la couche API, la base de données, l'ORM, l'authentification et les extensions. Pour ce tutoriel, choisissez :
- Frontend : React → TanStack Router
- Backend : Hono
- API : tRPC
- Runtime : Bun
- Base de données : SQLite
- ORM : Drizzle
- Auth : Better Auth
- Extensions : Turborepo, Biome
- Exemple : Todo (vous fournit une fonctionnalité de référence pour apprendre)
Vous préférez une commande unique et reproductible ? Passez les choix en drapeaux plutôt que de répondre aux invites :
bun create better-t-stack@latest my-app \
--frontend tanstack-router \
--backend hono \
--api trpc \
--runtime bun \
--database sqlite \
--orm drizzle \
--auth \
--addons turborepo biome \
--examples todo \
--installLe drapeau --install installe les dépendances automatiquement. Une fois terminé, vous disposez d'un monorepo complet. Il n'y a aucun verrouillage propriétaire ici — chaque dépendance est un paquet open source standard que vous auriez pu installer vous-même ; la CLI supprime simplement la corvée du câblage.
Vous pouvez aussi utiliser le Stack Builder visuel sur better-t-stack.dev/new pour cliquer vos choix et copier la commande générée. C'est le moyen le plus rapide d'explorer toute la matrice d'options sans mémoriser les noms des drapeaux.
Étape 2 : comprendre la structure générée
Placez-vous dans le projet et examinez l'agencement :
cd my-appUn monorepo généré typique ressemble à ceci :
my-app/
├── apps/
│ ├── web/ # frontend React
│ │ ├── src/
│ │ │ ├── routes/ # routes TanStack Router
│ │ │ ├── components/
│ │ │ └── utils/trpc.ts # client tRPC typé
│ │ └── package.json
│ └── server/ # backend Hono
│ ├── src/
│ │ ├── routers/ # routeurs tRPC
│ │ ├── db/ # schéma Drizzle + client
│ │ ├── lib/auth.ts # configuration Better Auth
│ │ └── index.ts # point d'entrée Hono
│ └── package.json
├── turbo.json # pipeline Turborepo
├── biome.json # configuration linter/formateur
└── package.json # racine du workspace
L'idée clé : apps/server exporte le type de son routeur tRPC, et apps/web importe ce type pour construire un client entièrement typé. Rien ne traverse le réseau sans type.
Étape 3 : lancer l'application pour la première fois
Configurez la base de données et démarrez les deux applications. Better-T-Stack câble Turborepo de sorte qu'une seule commande exécute tout le monorepo :
# Pousser le schéma Drizzle vers votre base SQLite locale
bun run db:push
# Démarrer web + serveur ensemble
bun devOuvrez l'URL affichée (généralement http://localhost:3001 pour l'application web). Si vous avez choisi l'exemple Todo, vous verrez une liste fonctionnelle adossée à l'API. Le serveur tourne sur son propre port (souvent 3000) et l'application web y proxifie les appels tRPC.
Si bun dev signale un fichier d'environnement manquant, copiez d'abord le .env.example généré vers .env dans apps/server. Better Auth a besoin d'un BETTER_AUTH_SECRET — générez-en un avec openssl rand -base64 32.
Étape 4 : ajouter une table de base de données avec Drizzle
Ajoutons maintenant notre propre fonctionnalité. Ouvrez le schéma Drizzle dans apps/server/src/db/schema et définissez une table notes :
// apps/server/src/db/schema/notes.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const notes = sqliteTable("notes", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
body: text("body").notNull().default(""),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
// Types inférés réutilisables partout
export type Note = typeof notes.$inferSelect;
export type NewNote = typeof notes.$inferInsert;Assurez-vous que le schéma est ré-exporté depuis le fichier d'agrégation (souvent apps/server/src/db/schema/index.ts), puis poussez le changement vers SQLite :
bun run db:pushDrizzle lit votre définition TypeScript et synchronise la table — sans fichier de migration SQL séparé à écrire à la main pour l'itération locale. Les types Note et NewNote sont désormais la source unique de vérité pour cette entité.
Étape 5 : exposer une procédure tRPC typée
Une procédure tRPC n'est qu'une fonction avec une entrée validée et une sortie typée. Créez un routeur pour les notes :
// apps/server/src/routers/notes.ts
import { z } from "zod";
import { desc } from "drizzle-orm";
import { router, publicProcedure } from "../lib/trpc";
import { db } from "../db";
import { notes } from "../db/schema/notes";
export const notesRouter = router({
list: publicProcedure.query(async () => {
return db.select().from(notes).orderBy(desc(notes.createdAt));
}),
create: publicProcedure
.input(
z.object({
title: z.string().min(1, "Le titre est requis").max(120),
body: z.string().max(2000).optional(),
}),
)
.mutation(async ({ input }) => {
const [created] = await db
.insert(notes)
.values({ title: input.title, body: input.body ?? "" })
.returning();
return created;
}),
});Le schéma z.object(...) valide l'entrée à l'exécution et infère le type d'entrée à la compilation. Si un appelant envoie la mauvaise forme, tRPC la rejette avant que votre gestionnaire ne s'exécute.
Enregistrez maintenant le routeur dans le routeur racine de l'application :
// apps/server/src/routers/index.ts
import { router } from "../lib/trpc";
import { notesRouter } from "./notes";
export const appRouter = router({
notes: notesRouter,
// ...autres routeurs (todo, etc.)
});
// Ce TYPE exporté est ce que consomme le frontend
export type AppRouter = typeof appRouter;Cette ligne export type AppRouter est toute l'astuce. Elle est effacée à la compilation — elle n'embarque aucun code d'exécution — et pourtant elle transporte la forme complète de chaque procédure à travers la frontière du paquet.
Étape 6 : consommer l'API depuis React
Côté frontend, le client tRPC généré importe déjà AppRouter. Vous appelez les procédures comme des fonctions asynchrones locales, avec autocomplétion complète et inférence du type de retour :
// apps/web/src/routes/notes.tsx
import { useState } from "react";
import { trpc } from "../utils/trpc";
export function NotesPage() {
const utils = trpc.useUtils();
const notesQuery = trpc.notes.list.useQuery();
const createNote = trpc.notes.create.useMutation({
onSuccess: () => utils.notes.list.invalidate(),
});
const [title, setTitle] = useState("");
return (
<div className="mx-auto max-w-xl p-6">
<form
onSubmit={(e) => {
e.preventDefault();
if (!title.trim()) return;
createNote.mutate({ title });
setTitle("");
}}
className="flex gap-2"
>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Titre de la nouvelle note"
className="flex-1 rounded border px-3 py-2"
/>
<button className="rounded bg-indigo-600 px-4 py-2 text-white">
Ajouter
</button>
</form>
<ul className="mt-6 space-y-2">
{notesQuery.data?.map((note) => (
<li key={note.id} className="rounded border p-3">
<strong>{note.title}</strong>
<p className="text-sm text-gray-500">{note.body}</p>
</li>
))}
</ul>
</div>
);
}Remarquez ce que vous n'avez pas fait : aucun fetch, aucune interface de réponse typée à la main, aucune chaîne d'URL d'API de base, aucun cast as. Survolez note dans votre éditeur et TypeScript indique Note — le type exact inféré à l'étape 4. C'est là toute la récompense de la stack.
Étape 7 : prouver que la sûreté de typage est réelle
Voici le test qui rend tout cela précieux. Retournez au schéma et renommez title en heading :
// apps/server/src/db/schema/notes.ts
heading: text("heading").notNull(), // était : titleSans toucher à rien d'autre, lancez une vérification de types sur tout le monorepo :
bun run check-typesTypeScript échoue le build et pointe à la fois vers la procédure serveur (qui référence toujours notes.title) et le composant React (qui lit note.title). L'incohérence est détectée à la compilation, dans l'éditeur, avant qu'une seule requête ne soit faite. Cette boucle de rétroaction — à travers la frontière réseau, en une seule vérification de types — est la valeur centrale que livre Better-T-Stack. Annulez le renommage vers title une fois les erreurs constatées.
Étape 8 : ajouter l'authentification avec Better Auth
Comme vous avez généré le projet avec --auth, Better Auth est déjà configuré dans apps/server/src/lib/auth.ts et monté dans l'application Hono. Pour protéger une procédure derrière une session, remplacez publicProcedure par le protectedProcedure généré :
import { protectedProcedure } from "../lib/trpc";
create: protectedProcedure
.input(z.object({ title: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
// ctx.session.user est typé et garanti d'exister ici
return db.insert(notes).values({
title: input.title,
body: "",
}).returning();
});Le middleware protectedProcedure vérifie la session et lève une erreur UNAUTHORIZED si l'utilisateur n'est pas connecté, de sorte que ctx.session est non nul dans votre gestionnaire. Better Auth fournit les endpoints de connexion, d'inscription et de session ; le client frontend (authClient) vous offre des helpers signIn, signUp et useSession typés prêts à l'emploi.
Tester votre implémentation
Vérifiez que la boucle complète fonctionne :
- Lancez
bun run db:pushet confirmez la création de la tablenotes(ouvrez le fichier SQLite avecbun run db:studiosi Drizzle Studio est câblé). - Lancez
bun devet ouvrez l'application web. - Ajoutez une note via le formulaire et confirmez qu'elle apparaît dans la liste et survit à un rafraîchissement de page (elle est persistée dans SQLite).
- Lancez
bun run check-types— il doit passer sans aucune erreur lorsque votre schéma et ses consommateurs concordent.
Dépannage
bun: command not found — Installez Bun, puis redémarrez votre shell pour que le chemin ~/.bun/bin soit chargé.
Les appels tRPC renvoient des erreurs CORS — Vérifiez que l'origine CORS du serveur correspond à l'URL de votre application web. Better-T-Stack définit une variable CORS_ORIGIN dans apps/server/.env ; mettez-la à jour si vous avez changé les ports.
BETTER_AUTH_SECRET is not defined — Générez-en un avec openssl rand -base64 32 et ajoutez-le à apps/server/.env.
Les changements de schéma ne sont pas répercutés — Relancez bun run db:push. Pour SQLite en début de développement, cela réécrit la table locale ; pour la production, utilisez plutôt drizzle-kit generate afin de produire des migrations versionnées.
Les types ne se mettent pas à jour dans l'éditeur — Redémarrez le serveur TypeScript (dans VS Code : palette de commandes → « TypeScript: Restart TS Server »). Le monorepo partage les types via les références de projet, qui ont parfois besoin d'un coup de pouce.
Étapes suivantes
- Remplacez SQLite par PostgreSQL avec un fournisseur hébergé (Neon, Supabase ou Turso) en régénérant avec
--database postgres --db-setup neon, ou ajustez votre configuration Drizzle manuellement. - Ajoutez oRPC au lieu de tRPC si vous voulez la génération OpenAPI en plus de la sûreté de typage — Better-T-Stack le prend en charge comme choix d'API interchangeable.
- Déployez
apps/serversur Cloudflare Workers en sélectionnant le runtime Workers ; la CLI configure Wrangler pour vous. - Explorez nos guides connexes : Drizzle ORM avec Next.js, tRPC avec l'App Router de Next.js, l'authentification avec Better Auth et construire des API REST avec Hono et Bun.
Conclusion
Better-T-Stack n'est pas un framework à apprendre — c'est un outil de génération qui assemble des bibliothèques indépendantes de premier ordre en un monorepo cohérent et typé de bout en bout. Le vrai gain, c'est la boucle de rétroaction vue à l'étape 7 : un changement dans le schéma de votre base de données se manifeste comme une erreur de compilation dans votre composant React, à travers la frontière réseau, avant que vous n'exécutiez quoi que ce soit. Vous avez choisi votre stack à la carte, sans aucun verrouillage propriétaire, et livré une fonctionnalité full-stack fonctionnelle en une seule session. À partir de là, chaque couche — frontend, API, ORM, auth — est un outil standard que vous pouvez étendre ou remplacer indépendamment.
Sources : create-better-t-stack sur GitHub, documentation Better-T-Stack, paquet npm.