casper's Profile Image

Full Stack Web/Mobile Developer

Nov, 1, 2024

How To Use Next Js 15 Api Routes With Prisma Mongodb To Make Full Stack Web Apps

You can make full stack web apps with using only Next Js api routes and interact with your database.

How To Use Next Js 15 Api Routes With Prisma Mongodb To Make Full Stack Web Apps Image

We can start the job by creating a brand new next js app.

I won't use Typescript because I want to make it beginner friendly.

npx create-next-app@latest
What is your project name: fullstack-nextjs-app

Would you like to use Typescript: No

Would you like to use ESLint: No

Would you like to use Tailwind CSS: Yes

Would you like your code inside a `src/` directory: No

Would you like to use App Router? (recommended): Yes

Would you like to use Turbopack for next dev?: Yes

Would you like to customize the import alias (@/* by default)?: No

When the installation finished you can open your project folder in Visual Studio Code or any ide editor you prefer.

yarn dev

Change the "globals.css" with the code below.

@tailwind base;
@tailwind components;
@tailwind utilities;

Change the "page.js" with the code below.

import CreatePost from "./components/CreatePost";

export default function Home() {
  return (
    <div className="max-w-3xl container mx-auto flex items-center justify-center min-h-screen">
      <div className="flex flex-col space-y-2">
        <h1 className="text-3xl font-bold">Full Stack Next Js App</h1>

        <CreatePost />
      </div>
    </div>
  );
}


Create a "components" folder inside app folder and create component named "CreatePost":

"use client";

import { useState } from "react";

const CreatePost = () => {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");

  const handleSubmit = () => {
    console.log("clicked!");
    // We will handle post create progress here.
  };

  return (
    <div>
      <div className="grid gap-6 mb-6 md:grid-cols-2">
        <div>
          <label
            htmlFor="title"
            className="block mb-2 text-sm font-medium text-gray-900"
          >
            Title
          </label>
          <input
            type="text"
            id="title"
            value={title}
            onChange={(event) => setTitle(event.target.value)}
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
            placeholder="Title"
            required
          />
        </div>

        <div>
          <label
            htmlFor="description"
            className="block mb-2 text-sm font-medium text-gray-900"
          >
            Description
          </label>
          <input
            type="text"
            id="description"
            value={description}
            onChange={(event) => setDescription(event.target.value)}
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
            placeholder="Description"
            required
          />
        </div>
      </div>

      <button
        onClick={handleSubmit}
        className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center"
      >
        Create
      </button>
    </div>
  );
};

export default CreatePost;

Our create post form ready, now we can create api routes for our form.

Create folder in app folder "api/posts/create" and create "route.js"

import { NextResponse } from "next/server";

export async function POST(request) {
  const body = await request.json();

  if (!body.title || !body.description) {
    return NextResponse.json(
      { error: "Title and description are required" },
      { status: 200 }
    );
  }

  try {
    const post = { ...body, id: Date.now().toString() };

    console.log("New post created:", post);

    return NextResponse.json(post, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to create post" },
      { status: 500 }
    );
  }
}

Now we have api route that we can handle the data we send from our form and handle errors.

Time to edit our "CreatePost" component to test the api route.

"use client";

import { useState } from "react";

const CreatePost = () => {
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [errorMsg, setErrorMsg] = useState("");

  const handleSubmit = async () => {
    try {
      const response = await fetch("/api/posts/create", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          title,
          description,
        }),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      if (data.error) {
        setErrorMsg(data.error);
      } else {
        console.log("Post created successfully:", data);
      }

      return data;
    } catch (error) {
      console.error("There was a problem with the fetch operation:", error);
    }
  };

  return (
    <div>
      {errorMsg ? (
        <div
          className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50"
          role="alert"
        >
          <span className="font-medium">Error:</span>
          {errorMsg}
        </div>
      ) : undefined}
      <div className="grid gap-6 mb-6 md:grid-cols-2">
        <div>
          <label
            htmlFor="title"
            className="block mb-2 text-sm font-medium text-gray-900"
          >
            Title
          </label>
          <input
            type="text"
            id="title"
            value={title}
            onChange={(event) => setTitle(event.target.value)}
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
            placeholder="Title"
            required
          />
        </div>

        <div>
          <label
            htmlFor="description"
            className="block mb-2 text-sm font-medium text-gray-900"
          >
            Description
          </label>
          <input
            type="text"
            id="description"
            value={description}
            onChange={(event) => setDescription(event.target.value)}
            className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
            placeholder="Description"
            required
          />
        </div>
      </div>

      <button
        onClick={handleSubmit}
        className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center"
      >
        Create
      </button>
    </div>
  );
};

export default CreatePost;

You can use the form to interact with api route. If you send empty form you'll get an error top of the form and if you fill the form fields you'll see created dummy data in your terminal console and also in your browser console.

Now time to initialize Prisma and connect to a MongoDB database.

Install Prisma CLI globally (if not already installed):

npm install -g prisma

Initialize Prisma in your project:

npx prisma init

This command will create a prisma directory with a schema.prisma file where you'll define your database schema.

Add "Post" model end of your schema.prisma file.

Don't forget to change your db provider from "postgresql" to "mongodb"

model Post {
  id        String   @id @default(auto()) @map("_id") @db.ObjectId
  title     String
  description String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Edit DATABASE_URL variable in your .env file in the root directory:

DATABASE_URL="your-mongodb-connection-string"

Push your Prisma schema to your database:

npx prisma db push

This command will generate automatically required files, if it didn't do manually:

npx prisma generate

Install Dependencies:

npm install @prisma/client

Edit the "/app/api/posts/create/route.js" file:

import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function POST(request) {
  const body = await request.json();

  if (!body.title || !body.description) {
    return NextResponse.json(
      { error: "Title and description are required" },
      { status: 200 }
    );
  }

  try {
    const post = await prisma.post.create({
      data: {
        title: body.title,
        description: body.description,
      },
    });

    return NextResponse.json(post, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to create post" },
      { status: 500 }
    );
  } finally {
    await prisma.$disconnect();
  }
}


Now you can create a post using the form we have.

You can check your database using the Prisma Studio:

npx prisma studio

Let's list the posts under the create post form.

Create "Posts" component inside the "app/components" folder:

"use client";

import { useEffect, useState } from "react";

const Posts = () => {
  const [posts, setPosts] = useState([]);

  const getPosts = async () => {
    try {
      const response = await fetch("/api/posts/get");

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      setPosts(data.posts);

      return data;
    } catch (error) {
      console.error("There was a problem with the fetch operation:", error);
    }
  };

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

  return (
    <div className="space-y-4 mt-8">
      <h2 className="text-2xl font-semibold">Posts ({posts.length})</h2>

      {posts.map((post) => (
        <div key={post.id} className="bg-blue-50 p-4 rounded-md">
          <p>{post.title}</p>
          <p>{post.description}</p>
        </div>
      ))}
    </div>
  );
};

export default Posts;

Add "Posts" component to "/app/page.js":

import CreatePost from "./components/CreatePost";
import Posts from "./components/Posts";

export default function Home() {
  return (
    <div className="max-w-3xl container mx-auto flex items-center justify-center min-h-screen">
      <div className="flex flex-col space-y-2">
        <h1 className="text-3xl font-bold">Full Stack Next Js App</h1>

        <CreatePost />

        <Posts />
      </div>
    </div>
  );
}


Create new api route "/app/api/posts/get/route.js":

import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function GET() {
  try {
    const posts = await prisma.post.findMany();

    return NextResponse.json({ posts });
  } catch (error) {
    return NextResponse.json({ error: "Failed to get posts" }, { status: 500 });
  } finally {
    await prisma.$disconnect();
  }
}

Now you can see the created posts in your home page.

Let's add a functionality to delete post.

Create "/app/api/posts/delete/[id]/route.js":

import { PrismaClient } from "@prisma/client";
import { NextResponse } from "next/server";

const prisma = new PrismaClient();

export async function DELETE(_request, context) {
  try {
    const params = await context.params;
    const id = params.id;

    // Validate the id
    if (!id) {
      return NextResponse.json({ error: "ID not found!" }, { status: 400 });
    }

    // Delete the post
    const deletedPost = await prisma.post.delete({
      where: {
        id,
      },
    });

    if (!deletedPost) {
      return NextResponse.json({ error: "Post not found" }, { status: 404 });
    }

    return NextResponse.json(
      { message: "Post deleted successfully" },
      { status: 200 }
    );
  } catch (error) {
    console.error("Error deleting post:", error);
    return NextResponse.json(
      { error: "Failed to delete post" },
      { status: 500 }
    );
  } finally {
    await prisma.$disconnect();
  }
}

Edit the "Posts" component:

"use client";

import { useEffect, useState } from "react";

const Posts = () => {
  const [posts, setPosts] = useState([]);

  const deletePost = async (id) => {
    try {
      const response = await fetch(`/api/posts/delete/${id}`, {
        method: "DELETE",
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      window.location.reload();
      return data;
    } catch (error) {
      console.error("There was a problem with the delete operation:", error);
    }
  };

  const getPosts = async () => {
    try {
      const response = await fetch("/api/posts/get");

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      setPosts(data.posts);

      return data;
    } catch (error) {
      console.error("There was a problem with the fetch operation:", error);
    }
  };

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

  return (
    <div className="space-y-4 mt-8">
      <h2 className="text-2xl font-semibold">Posts ({posts.length})</h2>

      {posts.map((post) => (
        <div key={post.id} className="bg-blue-50 p-4 rounded-md">
          <p>{post.title}</p>
          <p>{post.description}</p>

          <div className="flex">
            <p
              onClick={() => deletePost(post.id)}
              className="mt-2 bg-red-500 px-2 py-1 rounded-md text-xs text-gray-50 font-semibold cursor-pointer"
            >
              Delete
            </p>
          </div>
        </div>
      ))}
    </div>
  );
};

export default Posts;

Now you can list posts and delete each of them.

You can create edit functionality and pagination functionality too. Feel free to edit codes that we wrote.

0
0

Comments (0)