API
For the API layer nextstarter integrates tRPC (opens in a new tab).
Why tRPC:
tRPC is a modern RPC framework for TypeScript and gives you a way to define your API in a type-safe way. It also has a
lot of features like caching, batching, authorization and more. It also has a wide range of extensions like tRPC OpenAPI (opens in a new tab)
which you can use to generate an OpenAPI endpoint for your API.
Defining your API
All API endpoints are defined in the /packages/api/
library. In here you will find a modules
folder which contains
the different features modules of the API.
Create a new API endpoint
To create a new API endpoint you can either create a new module or add a new endpoint to an existing module.
Create a new module
To create a new module you can create a new folder in the modules
folder. For example modules/posts
. In this folder create a new sub-folder /procedures
with an index.ts
file.
Then create a new .ts file for the endpoint in the /procedures
folder. For example modules/posts/procedures/published-posts.ts
:
import { z } from 'zod';
import { publicProcedure } from '../../trpc';
import { db, PostSchema } from 'database';
export const publishedPosts = publicProcedure.output(z.array(PostSchema)).query(async () => {
const posts = await db.post.findMany({
where: {
published: true,
},
});
return posts;
});
Export endpoint from module
To export the endpoint from the module you need to add it to the /procedures/index.ts
file of the module:
export
from './published-posts';
Register module router
To make the module and it's endpoints available in the API you need to register a router for this module in the /modules/trpc/router.ts
file:
import
as postsProcedures from '../posts/procedures';
export const apiRouter = router({
// ...
posts: router(postsProcedures),
});
Use endpoint in frontend
How to use the endpoint in your application depends on whether you want to call the endpoint on the server or on the client.
Server components
To call the endpoint from a server component you need to create an api caller and call the endpoint like so:
import { createApiCaller } from 'api';
function MyServerComponent() {
const apiCaller = await createApiCaller();
const plans = await apiCaller.billing.plans();
// do something with the data...
return <div>{JSON.stringify(plans)}/div>;
}
Client components
To call the endpoint in the frontend you can import the apiClient
and use the endpoint like so:
import { apiClient } from '@shared/lib';
export function MyClientComponent() {
const { data: publishedPosts, isLoading: loadingPosts } = apiClient.posts.publishedPosts.useQuery();
if (loadingPosts) {
return <div>Loading...</div>;
}
return (
<div>
{data?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
Mutations
The above example shows how to create a query, which means to "get data" from the API. If you want to perform a mutation (e.g. create, update, delete), you create a mutation endpoint instead.
This looks very similar to the query:
import { z } from 'zod';
import { publicProcedure } from '../../trpc';
import { db, PostSchema } from 'database';
export const createPost = publicProcedure
.input(
z.object({
title: z.string(),
content: z.string(),
})
)
.output(PostSchema)
.mutation(async ({ input }) => {
const post = await db.post.create({
data: input,
});
return post;
});
As with the query procedure, export the mutation procedure from your feature module's index.ts
:
// ...
export * from './create-post.ts';
Use the mutation
To use the mutation you can use the apiClient
again:
import { apiClient } from '@shared/lib';
const formSchema = z.object({
title: z.string(),
content: z.string(),
});
type FormValues = z.infer<typeof formSchema>;
export function MyClientComponent() {
const createPostMutation = apiClient.posts.createPost.useMutation();
const {
handleSubmit,
register,
formState: { isSubmitting, isSubmitSuccessful },
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
});
const onSubmit: SubmitHandler<FormValues> = async ({ title, content }) => {
try {
await createPostMutation.mutateAsync({ title, content });
} catch {
// handle the error...
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
{...register("email")}
/>
<textarea
type="text"
{...register("content")}
/>
<button type="submit">
Create post
</button>
</div>
</form>
);
}