Insta-Next: Exploring APIs with React Queries

Insta-Next: Exploring APIs with React Queries

Let's start building the backend APIs using Next.js and use React Queries to fetch them!

In this part, we are going to first look into writing our first sets of APIs to fetch user details, posts and stories. Then, we will fetch them from the frontend using React Queries.

If you decided to skip to this part, no worry, I got you there, you can download the codes from the last part here. But it's best for you to check out the last part here to setup your Prisma and database.

Sneak Peek:

React Query

How do we usually make queries to API endpoints? Axios

Nothing's wrong with that, but we have React Query now! It enhances Axios like how Next.js made React better.

Why

I guess one big reason for using React Query is that we do not need to manage the states using useState anymore, and that means clean codes!

// no more
const SadAxiosComponent = () => {
  const [data, setData] = useState<SomeData[]>([]);
  useEffect(() => {
    axios.get(...).then((res) => {
      setData(res.data);
    });
  })
}
// Much better
const HappyComponent = () => {
  const query = useQuery({ queryKey: ['todos'], queryFn: getTodos });
  <>{query.data}</>
}

If that alone doesn't convince you, React queries will automatically refetch the data and cache them so that you don't need to pass them everywhere!

Usage

It's pretty easy to get started, we just define an Axios function (or some other asynchronous functions that returns a promise)

const getData = () => {
  return axios.get(...);
}

And pass the function as queryFN

// Don't mind me for repeating the codes here
const HappyComponent = () => {
  const query = useQuery({ queryKey: ['todos'], queryFn: getData });
  <>{query.data}</>
}

As simple as that! Of course, there are many other keys that we might explore later.

Building API Endpoints

If you look carefully, you can find an api folder under /src/pages, and that's where we define the APIs.

By default, there's a hello.ts inside the api folder, so this means that it's an endpoint for localhost:3000/api/hello, let's open it up

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

It defines a simple handler function that returns a JSON of name John Doe. It's actually pretty simple, let's try it out.

First, ramp up your development server

yarn dev

Then, head to the endpoint, localhost:3000/api/hello from either a browser, or if you have an API client like Postman, you can use it too

And.... voila! Exactly what we are expecting. Let's build a few more API endpoints to interact with our database.

Prisma Client

Before that, let's create a Prisma client that'll be used in our backend, I added a prisma.ts under src/utils

// src/utils/prisma.ts
import { PrismaClient } from "@prisma/client";

let prisma: PrismaClient;
// Fixing the type error
declare global {
  var prisma: PrismaClient;
}

if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  if (!global.prisma) {
    global.prisma = new PrismaClient();
  }
  prisma = global.prisma;
}

export default prisma;

This way, we only have a single Prisma client instance that we can import everywhere we need it. If there are multiple clients, some clients might not be able to reach the database due to the connection limit being exceeded.

Listing All Posts

Now that we have set up everything, let's create our first route, get /api/posts to read all posts.

Since there will be multiple endpoints under posts(e.g., /api/posts/1), we will make it a folder. So let's create a folder named posts in api, and our first endpoint will live in index.ts, I copied over the codes from hello.ts to here.

import { Post } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/utils/prisma";

type Data = {
  posts: Post[];
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const posts = await prisma.post.findMany();
  res.status(200).json({ posts });
}

Now, it's time to update the handler function to read all posts from Prisma, and return them.

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const posts = await prisma.post.findMany();
  res.status(200).json({ posts });
}

As expected, an error on the response type pops up

Let's quickly update the Data to return an object of Post[]

type Data = {
  posts: Post[];
};

The error is gone, life is good, and our first endpoint is done!

Let's head back to the browser, and navigate to localhost:3000/api/posts/

Pretty simple, isn't it?

Attaching Images to Models

Remember in the last part I mentioned that Prisma doesn't support polymorphism and I had to build it myself? So let's do it. Since this is an image feature, we will add a folder called images under feature folder.

Before that, I'll use a TypeScript trick to wrap the object with Image, to do this, I'll create this wrapper type, which will augment the type T provided with images.

// src/features/images/attach-image.ts
export type AttachImages<T> = T & {
  images: Image[]; // for posts & stories
};
/**
AttachImages<Post> = Post & {images: Image[]};
*/

But wait, users and stories only have a single image, and they have different keys. Let's fix it by providing a second parameter to the type

// src/features/images/attach-image.ts
export type AttachImage<T, Type extends string> = Type extends "user"
  ? T & { profile_pic?: Image } // for User's profile picture
  : Type extends "story"
  ? T & { image: Image } // for stories
  : T & {
      images: Image[]; // for posts
    };

A bit uglier, but that's the best I can do with my TypeScript knowledge. If you know better, do comment down and let me know!

Likewise, I'll create an attachImage function which finds all the images attached to the specific type

// src/features/images/attach-image.ts
export default async function attachImage<
  T extends { id: number },
  Type extends string
>(object: T, type: Type): Promise<AttachImage<T, Type>> {
  const images = await prisma.image.findMany({
    where: {
      associated_id: object.id,
      type,
    },
  });
  switch (type) {
    case "user":
      return {
        ...object,
        profile_pic: images?.[0],
      } as AttachImage<T, Type>;
    case "story":
      return {
        ...object,
        image: images?.[0],
      } as AttachImage<T, Type>;
  }
  return {
    ...object,
    images,
  } as AttachImage<T, Type>;
}

I had to add the line as AttachImage<T, Type> there, otherwise the IDE will scream for errors.

So there we go, a little TypeScript gimmicks here and there, and we got a perfect fully typed working function. Let's go back to our api/posts/index.ts and use this function to attach the image and update the response type there!

Disclaimer: This doesn't scale well when there are a lot of posts because I am making a query for every post, using SQL will be a better solution

// src/pages/api/posts/index.ts
export type Data = {
  posts: AttachImage<Post, "post">[];
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const posts = await prisma.post.findMany();

  // attachImage is an async function, so an extra step is needed
  // from mapping
  const postsWithImages = await Promise.all(
    posts.map(async (post) => await attachImage(post, "post"))
  );
  res.status(200).json({ posts: postsWithImages });
}

So far so good, let's make it better by extracting the logic into a separate function (We will practice clean codes)

// src/features/posts/findManyPosts.ts
import attachImage from "../images/attach-image";
import prisma from "@/utils/prisma";

const findManyPosts = async () => {
  const posts = await prisma.post.findMany();

  return await Promise.all(
    posts.map(async (post) => await attachImage(post, "post"))
  );
};

export default findManyPosts;

// src/pages/api/posts/index.ts
...
// I renamed the Data here to AllPostsData, will be needed later
export type AllPostsData = {
  posts: AttachImage<Post, "post">[];
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<AllPostsData>
) {
  const posts = await findManyPosts();
  res.status(200).json({ posts });
}

And... done! Let's head back to the browser and check localhost:3000/api/posts again

There we go, each post is attached with its respective images.

Challenge Time

Given the implementations for posts there, can you implement the same functions for stories and users?

As usual, you can find the completed solution on GitHub

localhost:3000/api/users

localhost:3000/api/stories

Querying from Frontend

Next, let's query these endpoints from frontend

Setting Up React Query

Let's install React Query

yarn add @tanstack/react-query

Following the documentation, we need to set up the _app.tsx to wrap the application in a provider too

// src/pages/_app.tsx
import "@/styles/globals.css";
import { AppProps } from "next/app";
import Head from "next/head";
import { MantineProvider } from "@mantine/core";
import React from "react";
import {
  Hydrate,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";

export default function App(props: AppProps) {
  const { Component, pageProps } = props;
  const [queryClient] = React.useState(() => new QueryClient());

  return (
    <>
      <Head>
        <title>Page title</title>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width"
        />
      </Head>

      <QueryClientProvider client={queryClient}>
        <Hydrate state={pageProps.dehydratedState}>
          <MantineProvider
            withGlobalStyles
            withNormalizeCSS
            theme={{
              colorScheme: "light",
            }}
          >
            <Component {...pageProps} />
          </MantineProvider>
        </Hydrate>
      </QueryClientProvider>
    </>
  );
}

And we're set to go!

Post Component

Here's a simple Post component that displays a post, and the images in a 3-column grid (we will make it nicer in upcoming parts)

// src/components/posts/Post.tsx
import { AttachImage } from "@/features/images/attach-image";
import { Image } from "@mantine/core";
import { Post } from "@prisma/client";

interface PostProps {
  post: AttachImage<Post, "post">;
}

const Post = ({ post: { caption, images, id } }: PostProps) => {
  return (
    <div>
      <p>{caption}</p>
      <div className="grid grid-cols-3">
        {images.map((image, index) => {
          return (
            // Some Mantine's quirk to set inner things to make image full
            <Image
              src={image.url}
              alt={caption}
              width="100%"
              key={index}
              className="aspect-square"
              height="100%"
              classNames={{ imageWrapper: "h-full", figure: "h-full" }}
            />
          );
        })}
      </div>
    </div>
  );
};

export default Post;

Querying

As mentioned earlier, React Query does require Axios under the hood, and it's often a good practice to write all Axios queries in an api folder, so I created an api folder under src. But before writing the queries, we gotta install it first

yarn add axios

Let's start writing our first query to retrieve posts

// src/api/posts.ts
import { AllPostsData } from "@/pages/api/posts";
import axios from "axios";

// remember the AllPostsData exported earlier? We will use it here
export const getAllPosts = async (): Promise<AllPostsData> => {
  const data = await axios.get("/api/posts");
  return data.data;
};

Then, we will display the posts in our index.tsx page, using React Query

// src/pages/index.tsx
import Head from "next/head";
import { useQuery } from "@tanstack/react-query";
import { getAllPosts } from "@/api/posts";
import Post from "@/components/posts/Post";

export default function Home() {
  // This is the important part
  const posts = useQuery({ queryFn: getAllPosts, queryKey: ["all-posts"] });
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <div>
          <!-- We will add skeletons later -->
          {posts.isSuccess &&
            posts.data.posts.map((post, index) => (
              <Post post={post} key={index} />
            ))}
        </div>
      </main>
    </>
  );
}

And that's it, we have completed our first fullstack page! It will query the data from backend, and display on the frontend. When you open up your localhost:3000, it should look like this, only with different images

Summary

And that's it! In this part, we have created 3 Get APIs to return users, posts and stories. We also used React Query's useQuery to fetch data from these APIs and display them accordingly.

I know it looks quite awful as of now, but bear with me, we will improve the UI in the next part!

Complete codes for this part can be found on GitHub