Contents
Introduction
So I am working on a side project, more on that soon, and as with a lot of projects I have been experimenting with lately I decided to start with Create T3 App. The boilerplate generated by Create T3 App is a great starting point for a lot of projects, and comes with a lot of the things I need out of the box. One of those things is TRPC, a great library for building type safe APIs in TypeScript lending itself to a great developer experience.
That being said, I have now u-turned on my decision to use TRPC for this project.
Why I abandoned TRPC
I have been using TRPC for a while now, and I have been enjoying it. It is a great library, and I have been able to build some great APIs with it. However, and I do think this is a common complaint, TRPC requires a fair bit of boilerplate in its set up. This boilerplate is not necessarily a bad thing, and you only really have to write it once. But for me (personally) I have found that it can sometimes be a lot of mental overhead to both get started and maintain.
Don’t get me wrong, I think TRPC is a great library, and I will continue to use it in the future. But for this project, I wanted something a little more lightweight.
Enter NextJS server actions
I have been following the development of NextJS server actions for a while now, and I have been really excited about them. They are described as:
Server Actions are asynchronous functions that are executed on the server. They can be used in Server and Client Components to handle form submissions and data mutations in Next.js applications.
Server actions allow “server components” in NextJS to use the "use server"
directive at the top of a functions body. Essentially, this allows you to write server side code in your NextJS app, and have it run on the server. See the example below:
// Server Component
export default function Page() {
// Server Action
async function create() {
'use server'
// ...
}
return (
// ...
)
}
Or you can even use server actions inside “client components” by writing your server action function inside a separate file placing the "use server"
directive at the top of the file. Then you can import the function into your client component and use it like so:
// src/app/actions.ts
"use server"
export async function create() {
// ...
}
import { create } from '@/app/actions'
export function Button() {
return (
// ...
)
}
One nice thing about this is it can mean your server actions are more maintainable as they are in their own file, and can be used in multiple components.
There are a lot more features to server actions, and I would recommend reading the NextJS documentation for more information. But as a quick break down:
- Server actions can be used within the form element using the
action
prop, also automatically receiving theFormData
object. - Optimistic updates are made easy in client components using server actions by using the
useOptimistic
hook provided by NextJS. - Server actions can be invoked from non server components like an
onClick
event. - They can be invoked inside of a
useEffect
hook.
This, for me at least, is a game changer. It is a much smaller API surface than TRPC, and it is built into NextJS. This means that I can use it without having to add any extra dependencies to my project.
And as a bonus… my head hurts a lot less!
So what about type safety?
One of the things I love about TRPC is the type safety it provides. I can define my API routes, and the types for the requests and responses. This is a great developer experience, and I wanted to keep that in my project.
Thankfully, this is why libraries like Zod exist. Zod is a TypeScript-first schema declaration and validation library. It allows you to define your schemas in TypeScript, and then use them to validate your data. This is great for form validation, and for defining the types of your server actions.
For example, you can define a schema like so:
import { z } from 'zod'
export const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
})
And then use it to validate your data like so:
import { UserSchema } from '@/app/schemas'
const user = {
name: 'John Doe',
email: 'john@example.com'
}
try {
UserSchema.parse(user)
} catch (error) {
console.error(error.errors)
}
This is great for form validation, and for defining the types of your server actions. You can even use Zod to define the types of your server actions, and then use them to validate the data that is passed to them.
Conclusion
So that is why I abandoned TRPC for this project. I wanted something a little more lightweight, and NextJS server actions fit the bill perfectly. They are built into NextJS, and provide a much smaller API surface than TRPC. And with the help of Zod, I can still have type safety in my project.