Ever since I got back into web dev around a year ago, I’ve been chasing the full-stack dream and learn to use a modern technology stack that would allow to build complete web apps from start to finish: frontend, backend, APIs, database and deployments.
The backend and API parts seemed pretty much figured out to me, after all, that’s where most of the problems for my game Tank Battle had to be solved. Other projects like LuccaBoard and LuccaChat allowed me to really put my React frontend skills to the test by building out complex UIs and backend systems using a MERN-style architecture (minus the MongoDB part) to make it truly full-stack. These last two projects were also a great way to learn several new technologies too: TypeScript for enhanced typesafety, Tailwind CSS for consistent and cohesive styling, among other cool things.
Problems
My hard work was paying off and I felt like I could now tackle more challenging problems and my development workflow improved too. But there were still some workflow and architectural problems that persisted:
- SEO and related metrics (FCP, TTFB) for Vite and CRA-based React apps suck because the app isn’t prerendered.
- Plain ol’ REST APIs don’t offer any typesafety at all. At best, this will just lead to wasting time fixing silly bugs, and at worst, you’ll screw up your entire client-side type inference and end up littering your TypeScript code with nasty
any
s and/or incorrect type assertions. - Authentication is pretty rough if you’re not using a full platform like Firebase. You have to either stick to a single provider/solution with good enough documentation like Google Identity’s one-tap login, or good luck navigating Passport.js’s docs.
- Spinning up Linux VMs like AWS Lightsail or DigitalOcean droplets to deploy your apps takes way too long. You gotta download Node.js (don’t forget to use nvm), set up and seed your database from scratch, configure NGINX, set up (and inevitably troubleshoot) your DNS settings, connect your auth providers, run your backend with a process manager like PM2… It’s insane!
T3 stack
The T3 stack is an opinionated technology stack for building end-to-end typesafe, full-stack Next.js web apps, heavily popularized by Theo Browne (T followed by 3 letters, hence the “T3”). The stack relies on six core technologies:
- TypeScript: type-safe programming language.
- Next.js: industry-standard React framework.
- Tailwind CSS: utility-first CSS framework.
- NextAuth.js: authentication for Next.js apps .
- Prisma: 3-in-1 TypeScript ORM, schema and migration tool for relational DBs.
- tRPC: end-to-end typesafe API builder for TypeScript apps.
What I really like about T3 is that it pretty much solves all of the problems I mentioned earlier:
- Next.js’s flexible rendering options, dynamic meta tags, and optimizations for images and fonts improve Lighthouse scores and SEO performance dramatically.
- tRPC provides typesafe API endpoints and return data by relying exclusively on native TypeScript type inference. It’s a full replacement for REST and GraphQL-based APIs.
- Integrating NextAuth with Next.js is pretty simple and you can easily add as many auth providers as you’d like.
- Next.js deployments on Vercel take full advantage of their serverless runtime and deploying is as each as pushing to
main
or merging PRs on GitHub.
Nexxel, with the help of Theo’s community, develops and maintains create-t3-app
, a CLI tool for scaffolding Next.js apps that lets you choose which of the tools above you’d like to use and sets everything up for you. The boilerplate generated by CT3A ties together all of the T3 technologies seamlessly, and it even has validation for environment variables with Zod! It’s a pretty great tool.
A note on serverless
Running apps in a serverless environment is fundamentally different to managing apps in Linux VMs. You don’t need to spin up new machines, set up a bazillion dependencies or even worry about scaling or load balancing. Your serverless functions will get invoked, scaled up or down, and destroyed automatically.
When you host your Next.js project to Vercel, your app’s API routes will be deployed as serverless functions, but you optionally choose to deploy them to an edge runtime too.
There are a couple of downsides to serverless computing, though:
- Arguably, the main issue would be the infamous cold start problem. Essentially, this is a delay that occurs when your function instance is created and initialized for the first time, which can make your site feel quite sluggish to users.
- Another problem to be aware of is that serverless functions are stateless by design: if you need long-running backend tasks that can’t be implemented using the simple request/response model (think CRON jobs or WebSockets), you’re out of luck.
- And of course, serverless pricing can be quite unpredictable. Unless you’re a solo dev or small team shipping product on Vercel’s Pro offering, AWS Lambda will eat away at your budget very quickly. Runaway Lambdas are also a very real danger, so beware.
The learning curve
That sounds all fine and dandy, but that’s a lot of stuff to learn. There are much easier ways to build full-stack apps (MERN stack included), and at the end of the day, developer experience is a convenience for developers, and it won’t always translate to a better user experience.
But speaking from personal experience, the investment is well worth it. Building apps with T3 not only introduces you to the serverless model, but you’ll squash a lot more bugs and ship stuff way faster. Let’s break it down.
TypeScript
Everyone should learn TypeScript. There, I said it. Some people on Tech Twitter will probably disagree with me on this take, but I strongly believe that TS’s strictness will make actually make you a better web developer.
I like to think of TS as a set of additional guardrails for you to hold on to. And trust me, when you start building more complex systems, you’ll appreciate any help you can get (especially when you’re dealing with networked software such as web apps). The best programming languages in the world have strong typing baked in for a reason—why should JavaScript, the native client-side scripting language of the web, be left out?
The truth is that good TypeScript code will look pretty much exactly like plain JavaScript. The key here is to add as few type annotations as possible and let the TS language server and type inference do the heavy lifting for you.
If you tried TS yet, read the handbook and give it a shot. Seriously, do it! You’ll be surprised at how much type safety you can squeeze out even if you just know the basics—silly type errors and mysterious undefined
s popping up in your code will be things of the past.
Next.js
Next.js is awesome. It’s everything I’ve ever wanted from a full-stack framework and more. Static site generation, server-side rendering, easy file-based routing, simple API routes (made even better by the Next.js tRPC adapter), tons of quality-of-life optimizations for fonts, images, CSS, etc. It’s basically React on steroids.
Next.js was surprisingly easy to learn thanks to the official Next.js tutorial. This guide showcases Next’s essential features and walks you through the Next.js development workflow by building a simple blog-style app.
Learning how to simultaneously think about running your code in the client (with React components) and in the server (with API routes and prerendering options) is arguably the hardest part of switching from React to Next. Learning to deal with the infamous hydration error bug is almost a rite of passage in that sense because it forces you to understand how SSR (one of Next’s main features) works at its core.
If Next.js feels a bit too much for you right now, I recommend trying Astro first. It’s a great framework for building high-quality websites with an HTML-first approach, and it’s got many of the same great features that Next has, including SSG, SSR, file-based routing (dynamic routes too), and you can even use React components if you want. I’ve recently used Astro to update my personal website and it certainly made learning Next.js a lot easier.
Tailwind CSS
Everyone has their favorite way to do styling nowadays: some like CSS-in-JS solutions like Emotion or styled-components, other prefer full UI libraries like MUI. I use Tailwind CSS for all of my projects. In fact, I use it so much that I’ve started taking it for granted.
I like using Tailwind CSS not only because it’s just CSS, bro™ except with utility classes, but because it already gives you a lightly opinionated system to start with. Tailwind’s defaults are well thought-out and make your styling more consistent, but can still be extended and/or overridden very easily if you wish. You’re free to design your components and build a design system (check out Class Variance Authority for that) without using cookie-cutter solutions like Bootstrap.
If you’re not keen on trying out any of the other things on this list, at the very least give Tailwind a try. Watch this great 90-minute tutorial from Brad Traversy to get started (or don’t?) and then through the Core Concepts articles in the Tailwind docs to get a feel for how utility classes can turbocharge your CSS.
Prisma
I’ve already talked plenty about Prisma, and I love it. The typesafe queries are awesome, the migration tools are great, and the declarative schema language makes modeling relational data easier than ever. It’s a much better developer experience compared to database drivers and writing raw SQL statements.
Now I’ll admit, Prisma’s learning curve is a bit steep—after all, you still need to learn how to accurately represent data relationships—but it’s not too bad either. Be sure to check out my tips for getting started with Prisma, head over to the docs, and start building stuff.
NextAuth.js
NextAuth is a unique auth solution in the sense that it essentially just requires you to set it up, add your auth providers, and retrieve your auth session information from a React Context provider. It’s that simple.
And it’s all entirely your control too, since it doesn’t rely on any 3rd-party services (other than providers, obviously) to function and properly authenticate your users. Essentially, you own your data.
NextAuth is still kinda finicky in some parts, and documentation is still a bit lacking in a few areas, such as customizing your login and error pages.
For the most part, though, NextAuth is by far the best self-hostable, React-based auth solution I’ve found so far. I recommend watching this video tutorial by Code Commerce to get you started, and then check out the official docs for reference materials and more guides.
TanStack Query
Although not technically one of the six core technologies of the T3 stack, TanStack Query (previously referred to as React Query) is used internally by tRPC as a data fetching and state management tool. It’s a really nice library that provides an opinionated but well thought-out way to fetch, cache, synchronize and update server state. Here’s a quick rundown of the main features by Fireship.
All actions in TanStack Query are referred to either as queries or mutations, kinda like GraphQL. The former refers to actions that fetch data from the server (i.e. read operations) while the latter refers to actions that alter server state (i.e. create, update and delete operations). Each action has special properties and methods that let you call the action asynchronously, monitor its status, retrieve its return data, invalidate the cached results, and much more.
TanStack Query does take some getting used to, though.
- For starters, you should use it as your main state management tool as much as possible. This means you’ll often be performing actions and storing their returned results at or near the lowest level of your component tree, instead of relying on global state managers like Redux Toolkit, Zustand, etc.
- You’ll also need to understand how the caching system works, as well as learn how to invalidate query chaches and when to invalidate them based on the mutations you’ve run.
- Depending on your use case, you’ll probably be interested in more advanced features such as paginated queries, optimistic updates, prefetching your queries ahead of time, among other things.
Web Dev Simplified published a 50-minute TanStack Query tutorial on YouTube which covers just about everything you need to know to get started with the library, but the official docs go into much more detail and are definitely worth reading.
tRPC
tRPC is probably the hardest piece of the T3 stack to learn, but once you get the hang of it it’ll feel like magic. I’m not joking here—the fact that you can write simple backend API endpoints with full typesafety for both the input and return data using TypeScript type inference alone is insane. There’s no codegen step at all. It’s just TypeScript, bro™.
Configuring tRPC and integrating the middleware functionality with NextAuth sessions feels a bit awkward to me, but fortunately, create-t3-app
already sorts that out for you.
When you’re building your backend API using tRPC, you’ll be creating procedures as your API endpoints. They’re just functions that take some input data, validate it (often with Zod), perform server-side CRUD actions, and return data. You can add stuff to procedures as you would in traditional REST frameworks, such as contexts for shared data (think session and DB connections) and middleware to run custom logic before or after your backend functions. Collections of procedures are known as routers and can be used to group common backend functionality.
You can then consume your API in your frontend with TanStack Query, using your typical queries, mutations, or subscriptions (a fancy term for WebSocket connections), with full type safety for procedure inputs and outputs. It’s important to note that tRPC has a special Next.js client that aids with SSR, SSG and other things.
Under the hood, tRPC just relies on good ol’ HTTP and REST endpoints, but it can also batch several operations together into a single request using the HTTP Batch Link, which is pretty neat and saves up some bandwidth too.
Web Dev Simplified has a great 45-minute tutorial to get you started with tRPC. And, of course, RTFM afterward.
Deployment bliss
The Next.js deployment and CI/CD experience on Vercel is unmatched. Link your GitHub repo, add environment variables, and you’ll automatically get deployments for your main branch (production), additional branches (previews) and your pull requests too (staging).
Currently, the best way to deploy Next.js apps (regardless of whether they’re built using the T3 stack) is through Vercel’s hosting platform, which caches your static pages to their CDN and deploys your Next.js API endpoints and backend code on AWS Lambda.
It’s that simple. You get several added benefits by deploying to Vercel:
- Your app is managed by Vercel’s DNS service.
- Statically generated pages are cached great Vercel’s CDN.
- By default, API routes are deployed as AWS Lambda serverless functions (edge functions are available too).
- Great integration with other Vercel services, such as Open Graph image generation, database storage, file uploads, and more.
Vendor lock-in?
Admittedly, there is some cause for concern about vendor lock-in here. Next.js, which is developed and maintained by Vercel, has become so closely intertwined with Vercel’s own platform and hosting offerings lately that, despite Next.js being an open-source project, it almost feels like Vercel is trying to lock you into their services here.
Other platforms like Netlify or AWS via SST, which do offer Next.js deployment and hosting services, have a hard time keeping up with Next’s latest features because of how vertically integrated Vercel’s products have become.
You could argue that Vercel is just way ahead of the curve here and that they’re pushing the limits of full-stack web development by offering innovative products and such. But a lot of people disagree, to the point where OpenNext, an open-source serverless adapter for Next.js, is being created to allow people to ditch the Vercel-specific Next.js features and deploy their Next apps elsewhere.
At the end of the day, it’s your call. I enjoy Vercel’s services (their free tier is quite generous!) and the developer experience offered by their platform is leagues ahead of the competition. But you’ll never see me complaining about having more options to choose from.
Missing features? More like modularity
You might have noticed that the T3 stack is missing some pieces. What database should I use? What about CRON jobs? Message queues? File uploads? Mailing system?
That’s the cool part about T3: you can use whatever you want. Unlike MERN, you don’t even have to use any of the six core technologies mentioned earlier—just maybe don’t skip out on TypeScript or Next.js! The rest is up to you and your specific business logic requirements. And if you’re using create-t3-app
, you’re in luck: your scaffolded app will only contain the integrations for the technologies you selected during setup.
That said, the T3 docs have some great recommendations for other things you might want to include in your stack, including databases, analytics, WebSockets, hosting and deployment options, component libraries, and much more.
Conclusion
I’ve been really impressed with the level of developer experience that T3 stack offers. It’s quite empowering too, and I’m not joking when I say that it actually does make you feel like a 10x developer hackerman. The amount of productivity you get and the quality of apps you can build with T3, especially for someone used to “plain” React development, is nothing short of amazing.
T3 makes your apps robust, scalable, and fully typesafe from database to data fetching. It even reduces some of the fragmentation commonly associated with React’s ecosystem by prescribing some opinionated approaches to solving common problems inherent to full-stack development. But it does so without chaining you down to your choices, unlike the MEAN stack.
It might not be everyone’s cup of tea, but you should give it a shot if you like React and TypeScript. Going forward, I’ll be using the T3 stack extensively as I keep learning about serverless and edge and building cool full-stack web apps.