DEV Community

Cover image for Next JS & Supabase CRUD🚀
Seif Eddine Saad
Seif Eddine Saad

Posted on

Next JS & Supabase CRUD🚀

Introduction:

Creating a CRUD (Create, Read, Update, Delete) application is a core skill for web developers, and using Next.js with Supabase makes it even easier. In this guide, we'll explore how to build a fully functional CRUD app using Next.js for the frontend and Supabase for the backend.

Demo:

https://next-supabase-crud-eight.vercel.app

Github:

Give it a star⭐️
https://github.com/seifeddinesaad01/Next-Supabase-CRUD

Requirement:

Before we go so far, let’s prepare some software we need.

Software

Account

1.Setup Project:

$ npm create next-app next14-supabase-crud
Enter fullscreen mode Exit fullscreen mode

2.Install Dependencies:

$ yarn add @supabase/supabase-js

Enter fullscreen mode Exit fullscreen mode

3. Supabase:

Create supabase project, fill the form

Image description

Then it will redirect into your project detail

Image description

4.Configure Environment Variable:

Let’s create .env file, and copy-paste this code below and define variable value based on supabase config.

NEXT_PUBLIC_SUPABASE_URL=supabase-url
NEXT_PUBLIC_SUPABASE_KEY=supabase-key

Enter fullscreen mode Exit fullscreen mode

5. Supabase configuration:

Then create supabase configuration file ./config/supabase.js, and copy paste code below.

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
export default supabase;

Enter fullscreen mode Exit fullscreen mode

6. Create Table Posts:

Create table named posts and define table column.Don’t forget to uncheck Enable Row Level Security (RLS) to make read and write access public.

Image description

Then:

Image description

7. Folder structure :

Image description

8. Create Page to List All Posts

Update page app/page.tsx and copy paste code below.

"use client"
import supabase from "../config/supabase";
import Head from "next/head";
import Link from "next/link";
import { useEffect, useState } from "react";
import { CiEdit } from "react-icons/ci";
import { MdDeleteOutline } from "react-icons/md";
import { Empty } from 'antd';



export default function Home() {
  const [posts, setPosts] = useState<any>([]);

  const getPosts = async () => {
    const { data, error } = await supabase.from('posts').select('*');
    if (error) {
      console.error('Error fetching data:', error.message);
    } else {
      setPosts(data);
    }
  }

  useEffect(() => {
    getPosts()
  }, [])

  return (
    <>
      <div className="container mx-auto mt-8 max-w-[560px] bg-white p-4 rounded-lg min-h-60">
        <div className="flex justify-between items-center pb-4 border-b border-dashed border-gray-900 mb-4">
          <h1 className="text-3xl font-semibold">Posts</h1>
          <Link
            className="bg-black hover:bg-opacity-80 text-white rounded-lg px-4 py-2 duration-200"
            href="/create"
          >
            Create New
          </Link>
        </div>
        <ul>
          {posts.map((task:any) => (
            <li key={task.id} className="py-2 flex justify-between w-full">
              <span>
                <strong>{task.title}</strong> - {task.description}
              </span>
              <span className="flex gap-2">
                <Link className="text-blue-700 underline hover:no-underline flex justify-center items-center gap-2" href={`/edit/${task.id}`}><CiEdit />Edit</Link>
                <Link className="text-red-500 underline hover:no-underline flex justify-center items-center gap-2" href={`/delete/${task.id}`}><MdDeleteOutline />Delete</Link>
              </span>
            </li>
          ))}
          {posts?.length < 1 && <Empty />}
        </ul>
      </div>
      <Head>
        <title>Post</title>
      </Head>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

9. Create Page to Create New Tasks

Create page app/create/page.jsx and copy paste code below.

"use client";

import supabase from "@/config/supabase";
import Head from "next/head";
import { useRouter } from "next/navigation"; // Update this import
import { useState } from "react";

export default function Create() {
  const router = useRouter(); // Use `useRouter` from `next/navigation`
  const [post, setPost] = useState<any>({
    title: "",
    description: "",
  });

  const onChange = (e: any) => {
    setPost({ ...post, [e.target.name]: e.target.value });
  };

  const handleCreate = async () => {
    try {
      await supabase.from("posts").upsert([post]);
      router.push("/"); // This line is fine
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      <div className="container mx-auto mt-8 max-w-[560px] bg-white p-4 rounded-lg min-h-60 ">
        <div className="flex justify-between items-center pb-4 border-b border-dashed border-gray-900 mb-4">
          <h1 className="text-3xl font-semibold">Create Post</h1>
        </div>
        <form>
          <div className="mb-4">
            <label>Title</label>
            <input
              className="mt-1 px-4 py-2 border border-gray-300 rounded-md block w-full"
              type="text"
              name="title"
              value={post?.tile}
              onChange={onChange}
            />
          </div>
          <div className="mb-4">
            <label>Description</label>
            <input
              className="mt-1 px-4 py-2 border border-gray-300 rounded-md block w-full"
              type="text"
              name="description"
              value={post?.description}
              onChange={onChange}
            />
          </div>
          <button
            className="bg-black hover:bg-opacity-80 text-white rounded-lg px-4 py-2 duration-200 w-full"
            type="button"
            onClick={handleCreate}
          >
            Create Post
          </button>
        </form>
      </div>
      <Head>
        <title>Create Post</title>
      </Head>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

10. Create Page to Edit Post

Create page app/edit/[id]/edit.tsx and copy paste code below.

"use client";

import supabase from "@/config/supabase";
import Head from "next/head";
import { useParams, useRouter } from "next/navigation"; // Use next/navigation for Next.js 13+
import { useEffect, useState } from "react";

interface Task {
  title: string;
  description: string;
}

const Edit = () => {
  const router = useRouter();

  const { id } = useParams();

  const [post, setPost] = useState<Task>({
    title: "",
    description: "",
  });

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPost({ ...post, [e.target.name]: e.target.value });
  };

  useEffect(() => {
    const fetchPost = async () => {
      if (typeof id === "string") {
        const { data, error } = await supabase
          .from("posts")
          .select("*")
          .eq("id", id)
          .single();

        if (error) {
          console.error("Error fetching task:", error.message);
          return;
        }

        if (data) {
          setPost(data);
        }
      }
    };

    if (id) {
      fetchPost();
    }
  }, [id]);

  const handleUpdate = async () => {
    try {
      if (typeof id === "string") {
        const { error } = await supabase.from("posts").update(post).eq("id", id);

        if (error) {
          console.error("Error updating task:", error.message);
          return;
        }

        router.push("/");
      }
    } catch (error) {
      console.error("Error:", error);
    }
  };

  return (
    <>
      <div className="container mx-auto mt-8 max-w-[560px] bg-white p-4 rounded-lg min-h-60">
        <div className="flex justify-between items-center pb-4 border-b border-dashed border-gray-900 mb-4">
          <h1 className="text-3xl font-semibold">Edit Post</h1>
        </div>
        <form>
          <div className="mb-4">
            <label htmlFor="name">Title: </label>
            <input
              className="mt-1 px-4 py-2 border border-gray-300 rounded-md block w-full"
              type="text"
              id="title"
              name="title"
              value={post.title}
              onChange={onChange}
            />
          </div>
          <div className="mb-4">
            <label htmlFor="description">Description: </label>
            <input
              className="mt-1 px-4 py-2 border border-gray-300 rounded-md block w-full"
              type="text"
              id="description"
              name="description"
              value={post.description}
              onChange={onChange}
            />
          </div>
          <button
            className="bg-black hover:bg-opacity-80 text-white rounded-lg px-4 py-2 duration-200 w-full"
            type="button"
            onClick={handleUpdate}
          >
            Edit Post
          </button>
        </form>
      </div>
      <Head>
        <title>Edit Post</title>
      </Head>
    </>
  );
};

export default Edit;

Enter fullscreen mode Exit fullscreen mode

11. Create Page to Delete Posts

Create page app/delete/[id]/page.tsx and copy paste code below.

"use client"
import supabase from "@/config/supabase";
import Head from "next/head";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";

const Delete = () => {
  const router = useRouter();
  const { id } = useParams();

  const [post, setPost] = useState<any>({
    title: "",
    description: "",
  });

  useEffect(() => {
    const fetchPost = async () => {
      const { data } = await supabase.from("posts").select("*").eq("id", id);
      setPost(data);
    };

    if (id) {
      fetchPost();
    }
  }, [id]);

  const handleDelete = async () => {
    try {
      await supabase.from("posts").delete().eq("id", id);
      router.push("/");
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      <div className="container mx-auto mt-8 max-w-[560px] bg-white p-4 rounded-lg min-h-60">
        <div className="flex justify-between items-center pb-4 border-b border-dashed border-gray-900 mb-4">
          <h1 className="text-3xl font-semibold">Delete Post</h1>
        </div>
        <form>
          <div className="my-12">
            Are you sure to delete <strong>{post?.name}</strong>?
          </div>
          <div className="flex w-full gap-2">
            <Link
              href="/"
              className="text-center bg-gray-300 hover:bg-opacity-80 text-black rounded-lg px-4 py-2 duration-200 w-full"
            >
              Cancel
            </Link>
            <button
              className="bg-black hover:bg-opacity-80 text-white rounded-lg px-4 py-2 duration-200 w-full"
              type="button"
              onClick={handleDelete}
            >
              Delete
            </button>
          </div>
        </form>
      </div>
      <Head>
        <title>Delete Post</title>
      </Head>
    </>
  );
};

export default Delete;

Enter fullscreen mode Exit fullscreen mode

12.Result:

npm run dev

Enter fullscreen mode Exit fullscreen mode

Your website would be like:

Image description

Image description

Image description

Image description

Conclusion

Congratulations on making it to the end of this guide on creating a CRUD application with Next.js and Supabase! I hope this article has given you a solid foundation and the confidence to build and expand your own web applications using these powerful tools. Thank you for taking the time to read through this tutorial—your interest and engagement mean a lot!

If you have any questions, feedback, or want to connect, feel free to reach out on LinkedIn or check out more of my projects on GitHub. I’d love to hear from you and see what amazing things you build!

Top comments (0)