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
- npm: (https://www.npmjs.com)
- Visual Studio Code: (https://code.visualstudio.com)
Account
- Supabase Account (https://supabase.com)
1.Setup Project:
$ npm create next-app next14-supabase-crud
2.Install Dependencies:
$ yarn add @supabase/supabase-js
3. Supabase:
Create supabase project, fill the form
Then it will redirect into your project detail
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
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;
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.
Then:
7. Folder structure :
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>
</>
);
}
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>
</>
);
}
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;
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;
12.Result:
npm run dev
Your website would be like:
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)