This TypeScript Library Helped Me Build Full-Stack Apps Faster

When I used to build web apps, I usually did things the same way. I'd set up a backend using Node.js and Express, creating standard REST API endpoints. It worked, but it felt like I was doing the same things over and over. For every new feature, I had to: Define the API route on the server. Figure out the data types for what the route needed and what it would send back. Go to the frontend code (usually React) and write code to call that specific API route. Define the types again on the frontend so it knew what data to expect. Often, I'd need extra tools like Redux just to manage fetching the data, showing loading spinners, and handling errors in React. This meant I had the same information about my API (the path, the data types) in two places: the backend and the frontend. If I changed something on the backend – like having an API send back different data – I had to remember to update the frontend code too. If I forgot, things would break. As a one-person team trying to build things quickly, keeping everything matched up was slow and a bit annoying. I'm an indie hacker building UserJot and I needed a better way to move fast, make sure things work, and keep my code clean. So, finding something to make this easier was a big deal. Finding a Simpler Way: tRPC Then, in 2023, I came across a TypeScript library called tRPC. The main idea sounded really neat: what if your frontend code could just call your backend code directly, like calling a normal function, and all the data types would automatically match up? I wasn't sure if it was just hype, but it sounded interesting enough to try. I spent about a week playing with it on a small project. And honestly? It made things much simpler and faster for me. How It Works: A Quick Look With tRPC, you don't really think about REST endpoints in the same way. Instead, you create "procedures" on your backend. Think of them like functions your frontend can call. You can use helper libraries like Zod to clearly define what kind of data goes into these procedures and what comes out. Here’s a basic idea: Backend (Server Code): // server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; // Zod helps define types const t = initTRPC.create(); // Define the functions our frontend can call export const appRouter = t.router({ // A procedure to get user data getUser: t.procedure .input(z.object({ userId: z.string() })) // Needs a userId string .query(({ input }) => { // Code to find the user... return { id: input.userId, name: 'Example User' }; // Returns user object }), // A procedure to create a new user createUser: t.procedure .input(z.object({ name: z.string() })) // Needs a name string .mutation(({ input }) => { // Code to create the user... return { id: 'newUser123', name: input.name }; // Returns new user }), }); // Export the type of our router export type AppRouter = typeof appRouter; Here we made two procedures: getUser and createUser. Zod checks the input data. The important part is exporting the AppRouter type – this tells TypeScript exactly what our API looks like. Frontend (Client Code using React): tRPC works really well with frontend tools like React Query. You set up a tRPC client on the frontend and tell it about the AppRouter type you exported from the backend. // client.tsx (React example) import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './server'; // Import the type from the backend! const trpc = createTRPCReact(); function UserProfile() { // This hook knows exactly what 'getUser' needs and returns! const userQuery = trpc.getUser.useQuery({ userId: '123' }); if (userQuery.isLoading) return Loading...; if (userQuery.error) return Error: {userQuery.error.message}; // We know `userQuery.data` has a `name` property because of tRPC! return User Name: {userQuery.data.name}; } function CreateUserButton() { const mutation = trpc.createUser.useMutation(); const handleCreate = () => { // tRPC checks if we're sending the right input (`{ name: string }`) mutation.mutate({ name: 'New User' }); }; return Create User; } See how the frontend imports the AppRouter type? Because of that, the trpc.getUser.useQuery hook automatically knows it needs a userId and that its data will have an id and name. You don't have to write fetch calls or type definitions for the API data on the client anymore. It just knows! What Really Got Better for Me Using tRPC solved my main headaches: Faster Building: I stopped writing the same API info twice. Making a function on the backend meant it was instantly ready and typed on the frontend. This let me build features much quicker. Fewer Bugs: Because the types are shared, TypeScript would tell me immediately if I messed something up. If I changed the backend function to return different data, my frontend code would show an error right

Apr 11, 2025 - 17:37
 0
This TypeScript Library Helped Me Build Full-Stack Apps Faster

When I used to build web apps, I usually did things the same way. I'd set up a backend using Node.js and Express, creating standard REST API endpoints. It worked, but it felt like I was doing the same things over and over.

For every new feature, I had to:

  1. Define the API route on the server.
  2. Figure out the data types for what the route needed and what it would send back.
  3. Go to the frontend code (usually React) and write code to call that specific API route.
  4. Define the types again on the frontend so it knew what data to expect.
  5. Often, I'd need extra tools like Redux just to manage fetching the data, showing loading spinners, and handling errors in React.

This meant I had the same information about my API (the path, the data types) in two places: the backend and the frontend. If I changed something on the backend – like having an API send back different data – I had to remember to update the frontend code too. If I forgot, things would break. As a one-person team trying to build things quickly, keeping everything matched up was slow and a bit annoying.

I'm an indie hacker building UserJot and I needed a better way to move fast, make sure things work, and keep my code clean. So, finding something to make this easier was a big deal.

Finding a Simpler Way: tRPC

Then, in 2023, I came across a TypeScript library called tRPC. The main idea sounded really neat: what if your frontend code could just call your backend code directly, like calling a normal function, and all the data types would automatically match up? I wasn't sure if it was just hype, but it sounded interesting enough to try. I spent about a week playing with it on a small project.

And honestly? It made things much simpler and faster for me.

How It Works: A Quick Look

With tRPC, you don't really think about REST endpoints in the same way. Instead, you create "procedures" on your backend. Think of them like functions your frontend can call. You can use helper libraries like Zod to clearly define what kind of data goes into these procedures and what comes out.

Here’s a basic idea:

Backend (Server Code):

// server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // Zod helps define types

const t = initTRPC.create();

// Define the functions our frontend can call
export const appRouter = t.router({
  // A procedure to get user data
  getUser: t.procedure
    .input(z.object({ userId: z.string() })) // Needs a userId string
    .query(({ input }) => {
      // Code to find the user...
      return { id: input.userId, name: 'Example User' }; // Returns user object
    }),

  // A procedure to create a new user
  createUser: t.procedure
    .input(z.object({ name: z.string() })) // Needs a name string
    .mutation(({ input }) => {
      // Code to create the user...
      return { id: 'newUser123', name: input.name }; // Returns new user
    }),
});

// Export the type of our router
export type AppRouter = typeof appRouter;

Here we made two procedures: getUser and createUser. Zod checks the input data. The important part is exporting the AppRouter type – this tells TypeScript exactly what our API looks like.

Frontend (Client Code using React):

tRPC works really well with frontend tools like React Query. You set up a tRPC client on the frontend and tell it about the AppRouter type you exported from the backend.

// client.tsx (React example)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './server'; // Import the type from the backend!

const trpc = createTRPCReact<AppRouter>();

function UserProfile() {
  // This hook knows exactly what 'getUser' needs and returns!
  const userQuery = trpc.getUser.useQuery({ userId: '123' });

  if (userQuery.isLoading) return <div>Loading...</div>;
  if (userQuery.error) return <div>Error: {userQuery.error.message}</div>;

  // We know `userQuery.data` has a `name` property because of tRPC!
  return <div>User Name: {userQuery.data.name}</div>;
}

function CreateUserButton() {
  const mutation = trpc.createUser.useMutation();

  const handleCreate = () => {
    // tRPC checks if we're sending the right input (`{ name: string }`)
    mutation.mutate({ name: 'New User' });
  };

  return <button onClick={handleCreate} disabled={mutation.isLoading}>Create User</button>;
}

See how the frontend imports the AppRouter type? Because of that, the trpc.getUser.useQuery hook automatically knows it needs a userId and that its data will have an id and name. You don't have to write fetch calls or type definitions for the API data on the client anymore. It just knows!

What Really Got Better for Me

Using tRPC solved my main headaches:

  1. Faster Building: I stopped writing the same API info twice. Making a function on the backend meant it was instantly ready and typed on the frontend. This let me build features much quicker.
  2. Fewer Bugs: Because the types are shared, TypeScript would tell me immediately if I messed something up. If I changed the backend function to return different data, my frontend code would show an error right away in my editor, before I even ran the app.
  3. Easier Coding: My code editor could autocomplete frontend API calls correctly. And tools like React Query handled all the tricky parts of data fetching (like caching and loading states) easily because tRPC worked so well with them. I didn't need Redux just for server data anymore.

How I Use It Now

I use tRPC for almost everything now, including UserJot's entire backend. When I make backend procedures like createSubmission, I get easy-to-use, type-safe functions on the frontend right away. It's not just for small toys; it works well for building real applications, at large scale.

When tRPC Might Not Be the Right Fit

tRPC is fantastic when you control both the frontend and backend, and you're using TypeScript (or JavaScript) for both. It ties them together nicely.

But, if you're building an API for other people to use (a public API), something like REST or GraphQL is probably better. Those are more standard ways for different systems (that you don't control) to talk to each other.

Maybe Give It a Try?

If you’re building full-stack apps with TypeScript and feel like you spend too much time just keeping frontend and backend types in sync, maybe give tRPC a look. It definitely made my development process smoother and faster—especially when building and scaling a tool like UserJot, where I can’t afford to waste time duplicating types or debugging mismatches.

Also, if you're looking for a customer feedback tool, I'm building UserJot and would love to hear what you think.