Next.js
API

API

For the API layer supastarter 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:

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:

index.ts
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:

router.ts
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:

create-post.ts
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>
  );
}