Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/iamhectorsosa/trpc-basic-starter
tRPC Basic Starter
https://github.com/iamhectorsosa/trpc-basic-starter
blog nextjs typescript vercel
Last synced: 29 days ago
JSON representation
tRPC Basic Starter
- Host: GitHub
- URL: https://github.com/iamhectorsosa/trpc-basic-starter
- Owner: iamhectorsosa
- Created: 2022-10-10T11:43:02.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2022-10-17T09:59:29.000Z (about 2 years ago)
- Last Synced: 2023-05-24T11:23:49.816Z (over 1 year ago)
- Topics: blog, nextjs, typescript, vercel
- Language: TypeScript
- Homepage: https://trpc-basic-starter.vercel.app
- Size: 130 KB
- Stars: 1
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Local Setup + Development
```
$ npx create-next-app --example https://github.com/ekqt/trpc-basic-starter trpc-basic-starter
$ cd trpc-basic-starter# <- Using `npm`
$ npm i
$ npm run dev# <- Using `yarn`
$ yarn dev
```[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ekqt/trpc-basic-starter?file=src/pages/index.tsx&file=src/server/routers/user.ts&view=editor)
# tRPC: Understanding Typesafety
Typesafety is the extent to which a programming language prevents type errors. The process of verifying and enforcing the constraints of types may occur at compile time or at run-time. A programming language like [TypeScript](https://typefully.com/) checks a program for errors before execution (at compile time) as a static type checker. In contrast, a library like [Zod](https://zod.dev/) can also provide you type checking at run-time. So how does a library like tRPC helps us better understand typesafety?
> tRPC allows you to easily build and consume fully typesafe APIs, without schemas or code generation.
At its core, [tRPC](https://trpc.io/) provides the solution to statically type our API endpoints and share those types between our client and server, enabling type safety from end-to-end.
## How does tRPC share types between client/server?
Types are shared based on one or many _procedures_ contained in Routers. A **procedure** is a _composable_ query, mutation or subscription where you define how your client/server interact with each other.
Let's see what you'd need to create a query procedure for a [Next.js application](https://trpc.io/docs/v10/nextjs). We'll explore these concepts by reviewing our [tRPC-basic-starter](https://github.com/ekqt/trpc-basic-starter) GH repo. Here's how our file structure initially looks like:
```graphql
# @path: ./src
├── pages
│ └── api/trpc
│ └── [trpc].ts # <-- tRPC HTTP handler
│ └── _app.tsx # <-- tRPC Provider
│ └── index.tsx
│ └── [...]
├── server
│ └── routers
│ └── _app.ts # <-- Main app router
│ └── user.ts # <-- User sub-router
│ └── [...] # <-- More sub-routers
│ └── trpc.ts # <-- Procedure helpers
├── utils
│ └── trpc.ts # <-- Typesafe tRPC hooks
```You could define all of your procedures within the **tRPC HTTP handler** and completely skip the server directory. However, this wouldn't be a scalable approach. It's very likely that your backend will require several endpoints, for which it is recommended to separate your procedures into different sub-routers and merge them as suggested in the file structure above.
To create a query procedure, at the most basic level we need to define an `input` (optional and validated with your library of choice), and a `query` (the actual implementation of the procedure) which runs a function returning the data you need. At the end, the types of each router are exported to provide a fully-typed experience on the client without importing any server code.
```typescript
// @path: ./src/server/routers/user.ts
import { t } from "../trpc";
import { z } from "zod";export const userRouter = t.router({
// Define a procedure (function) that
// ...takes an input and provides a query
// ...as `user.greet.useQuery()`
greet: t.procedure
// Input validation
.input(z.object({ name: z.string() }))
.query(({ input }) => {
// Here you would process
// any information you'd need
// ..to return it to your client
return { text: `Hello, ${input.name}!` };
}),
});
```## Using your new tRPC-backend on the client
[@tRPC/react](https://trpc.io/docs/v10/react-queries) provides a set of hooks wrapped around [@tanstack/react-query](https://tanstack.com/query/v4/docs/guides/queries), so under the hood, they work just the same to fetch data from a server. You'll notice that the conventional _querying keys and functions_ are defined within your procedure.
```tsx
// @path: ./src/pages/index.tsx
import { trpc as t } from "../utils/trpc";export default function Home() {
// Wrapped around @tanstack/react-query
// Can also destructure to access
// isLoading, isError, isSuccess, error and data
const result = t.user.greet.useQuery({ name: "Client" });if (result.isLoading) {
return (
Loading...
);
}return (
{result.data?.text}
);
}
```...and that's the basic setup. Both the result and input are type-inferred from the procedures as defined and will get **TypeScript autocompletion and IntelliSense** that matches your backend API without requiring any code generation.
## More examples of tRPC usage
Let's create an additional sub-router `@path: ./src/server/routers/post.ts` where we need to provide our client with the following: (a) fetch all posts, (b) fetch post by ID, (c) create a new post. Notice that requirements for `a` and `b` are different than for `c`, as the first two are query procedures and the last one requires a **mutation procedure**. However, you will notice that there is _no difference_ between queries and mutation apart from semantics.
```typescript
import { t } from "../trpc";
import { z } from "zod";type PostType = {
userId: number;
id: number;
title: string;
body: string;
};export const postRouter = t.router({
// Define a procedure that
// ...doesn't require an input and provides a query
// ...as `post.allPosts.useQuery()`
allPosts: t.procedure.query(async () => {
const allPosts = await fetch(
"https://jsonplaceholder.typicode.com/posts"
).then((response) => response.json());
return { posts: posts as Array };
}),
// Define a procedure that
// ...takes an id and provides a query
// ...as `post.postById.useQuery({ id })`
postById: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const post = await fetch(
`https://jsonplaceholder.typicode.com/posts/${input.id}`
).then((response) => response.json());
return { post: post as PostType };
}),
});
```## How can we use procedures?
[Procedures](https://trpc.io/docs/v10/quickstart#add-a-query-procedure) are able to resolve any custom function to process a validated `{ input }`. Just to name a few examples: you could make use of an ORM like [Prisma](https://www.prisma.io/), a Baas like [Supabase](https://supabase.com), or a headless CMS like [Sanity](https://www.sanity.io/) to process your data with the benefits of fully typesafe APIs.
## What about Mutations?
[Mutations](https://tanstack.com/query/v4/docs/guides/mutations) are typically used to create/update/delete data. Let's take a look on how we could create a mutation procedure using our Post sub-router:
```typescript
import { t } from "../trpc";
import { z } from "zod";type PostType = {
userId: number;
id: number;
title: string;
body: string;
};export const postRouter = t.router({
// Define a procedure that
// ...takes an id and executes a mutation
// ...as `post.createPost.useMutation()`
// ...from `*.mutate({ input })`
createPost: t.procedure
.input(
z.object({
title: z.string().min(1),
body: z.string().min(1),
userId: z.number(),
})
)
.mutation(async ({ input }) => {
const post = await fetch(
"https://jsonplaceholder.typicode.com/posts",
{ method: "POST", body: JSON.stringify(input) }
).then((response) => response.json());
return { response: post as PostType };
}),
});
```Here is how we could execute that mutation from the client:
```tsx
import { FormEvent, ChangeEvent, useState } from "react";
import { trpc as t } from "../../utils/trpc";export default function NewPost() {
// User is fixed for simplicity
const initValue = { title: "", body: "", userId: 1 };
const [form, setForm] = useState(initValue);
const mutation = t.post.createPost.useMutation();function handleChange(
e: ChangeEvent
) {
setForm({ ...form, [e.target.id]: e.target.value });
}function handleSubmit(e: FormEvent) {
e.preventDefault();
mutation.mutate(form);
setForm(initValue);
}return (
Title
Body
Submit
);
}
```> Mutations are as simple to do as queries, they're actually the same underneath, but are just exposed differently as syntactic sugar and produce a HTTP POST rather than a GET request.
## So WIFY by using tRPC?
What's in it for you using tRPC? We barely scratched the surface, here are a list of features:
- Full static typesafety with autocompletion on the client, inputs, outputs and errors.
- No code generation, run-time bloat, or build pipeline.
- Zero dependencies and a tiny client-side footprint.
- All requests are able to be batched automatically into one.
- Framework agnostic and compatible with all JavaScript frameworks and runtimes.Feel free to explore this example using the resources below:
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ekqt/trpc-basic-starter?file=src/pages/index.tsx&file=src/server/routers/user.ts&view=editor)
Originally published: [tRPC: Understanding Typesafety](https://www.webscope.io/blog/trpc-understanding-typesafety)