Build a fullstack web app
Tech stack used in this tutorial
GitHub repo
Content
Introduction
Initial setup
Install shadcn/ui
Create a db using Docker
Install Prisma
Config Auth.js
Improve your UI
Add CRUD functionality
Conclusion
1. Introduction
In this tutorial, we will develop a fullstack web app with the following tech
stack:
We’ll learn some of the fundamentals of this tech stack, like using server
components in Next.js, or creating API endpoints using the app router .
2. Initial setup
Let’s start creating a new Next.js project, in your terminal run:
Terminal npx create-next-app@latest
Make sure to mark Yes the following options:
Would you like to use TypeScript ?
Would you like to use ESLint ?
Would you like to use Tailwind CSS ?
Would you like to use ‘src/’ directory ?
Would you like to use App Router ? (recommended)
Terminal > What is your project named ? fullstack-app
> Would you like to use TypeScript ? No / Yes
> Would you like to use ESLint ? No / Yes
> Would you like to use Tailwind CSS ? No / Yes
> Would you like to use ` src/ ` directory? No / Yes
> Would you like to use App Router ? ( recommended ) No / Yes
> Would you like to customize the default import alias ( @/* ) ? No / Yes
> What import alias would you like configured ? @/ *
Wait until the dependencies installation is completed, then access to the
project directory:
Terminal cd fullstack-app
Open your code editor of your preference.
2.1 Install shadcn/ui
This components will help us a lot building the UI along with TailwindCSS.
First, initialize shadcn/ui:
Terminal npx shadcn-ui@latest init
Make sure to config shadcn/ui according your project configuration.
You can check the shadcn/ui docs for every
component that you could need, each components is installed individually.
2.2 Create a db using Docker
Make sure to have Docker installed in your machine.
First you need to pull a PostgreSQL image from Docker Hub:
Terminal docker pull postgres
Then, create a container with the image:
Terminal docker run --name my-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres
2.3 Install Prisma
Install Prisma using your dependency manager, in this case npm :
Terminal npm install prisma -D
Now initialize Prisma:
Terminal npx prisma init
A new ./prisma
direcotry will be created in the root of your project, with a
schema.prisma file.
You’ll create your schemas in this file.
Add this model as an example:
prisma/schema.prisma model User {
id Int @id @default ( autoincrement ())
createdAt DateTime @default ( now ())
email String @unique
name String ?
}
Update your .env
file with the following:
.env DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?schema=public"
In the URL is your username (by default is postgres), your password (in this
case password), the host (by default is localhost), the port (by default is
5432), the database name (by default is postgres) and the schema (by default is
public).
Create your first migration to test if Prisma can connect to your local
database:
Terminal npx prisma migrate dev --name init
If everything is ok, you’ll see a new /migrations directory with a new file
inside.
If you have an error, make sure you can connect to your local DB. Delete, and
create the container again if necessary.
2.4 Config Auth.js
Add this models to your prisma schema :
prisma/schema.prisma model Account {
id String @id @default ( cuid ())
userId String
type String
provider String
providerAccountId String
refresh_token String ? @db.Text
access_token String ? @db.Text
expires_at Int ?
token_type String ?
scope String ?
id_token String ? @db.Text
session_state String ?
user User @relation ( fields : [userId], references : [id], onDelete : Cascade )
@@unique ( [provider, providerAccountId] )
}
model Session {
id String @id @default ( cuid ())
sessionToken String @unique
userId String
expires DateTime
user User @relation ( fields : [userId], references : [id], onDelete : Cascade )
}
model User {
id String @id @default ( cuid ())
name String ?
email String ? @unique
emailVerified DateTime ?
image String ?
accounts Account []
sessions Session []
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique ( [identifier, token] )
}
These models are for Auth.js , now we can install it with the prisma adapter:
Terminal npm install @prisma/client @auth/prisma-adapter
Instal nodemailer too, as we’ll use magic links for authentication:
Terminal npm install nodemailer -D
Now create a src/utils/db.ts
and initialize prisma :
src/utils/db.ts import { PrismaClient } from '@prisma/client' ;
const prismaClientSingleton = () => {
return new PrismaClient () ;
};
declare global {
var prisma : undefined | ReturnType < typeof prismaClientSingleton > ;
}
const prisma = globalThis . prisma ?? prismaClientSingleton () ;
export default prisma ;
if (process . env . NODE_ENV !== 'production' ) globalThis . prisma = prisma ;
Then, create a src/utils/auth.ts
file to config Auth.js :
src/utils/auth.ts import type { NextAuthOptions } from 'next-auth' ;
import { PrismaAdapter } from '@auth/prisma-adapter' ;
import EmailProvider from 'next-auth/providers/email' ;
import prisma from '@/libs/db' ;
import { Adapter } from 'next-auth/adapters' ;
export const authOptions = {
adapter : PrismaAdapter (prisma) as Adapter ,
providers : [
EmailProvider ( {
server : {
host : process . env . EMAIL_SERVER_HOST ,
port : process . env . EMAIL_SERVER_PORT ,
auth : {
user : process . env . EMAIL_SERVER_USER ,
pass : process . env . EMAIL_SERVER_PASSWORD ,
},
},
from : process . env . EMAIL_FROM ,
} ) ,
] ,
callbacks : {
session : async ({ session , user }) => {
return {
... session ,
user : user ,
};
},
},
} satisfies NextAuthOptions ;
This config is for using an email provider, for this project we’ll use
Resend .
Create an account and get the next credentials in your .env file:
EMAIL_SERVER_HOST: smtp.resend.com
EMAIL_SERVER_PORT: 465
EMAIL_SERVER_USER: resend
EMAIL_FROM: onboarding@resend(dot)dev
EMAIL_SERVER_PASSWORD: yor api key
Now, create a src/app/api/auth/[...nextauth]/route.ts
file:
src/app/api/auth/[...nextauth]/route.ts import { authOptions } from '@/libs/auth' ;
import NextAuth from 'next-auth/next' ;
const handler = NextAuth (authOptions) ;
export { handler as GET , handler as POST };
This file is for handling the authentication in our app.
You can now authenticate users with a magic link sent by email.
Create a src/app/auth/signin-form.tsx
file:
src/app/auth/signin-form.tsx 'use client' ;
import { useState } from 'react' ;
import { signIn } from 'next-auth/react' ;
export default function SigninForm () {
const [ email , setEmail ] = useState < null | string > ( null ) ;
async function handleSubmit () {
await signIn ( 'email' , {
email ,
callbackUrl : ` ${ window . location . origin } ` ,
} ) ;
}
return (
< form className = 'mt-5 space-y-4' action = { handleSubmit } >
< section className = 'flex flex-col gap-2' >
< label htmlFor = 'email' > Email </ label >
< input
id = 'email'
type = 'email'
name = 'email'
onChange = {( e ) => setEmail (e . target . value) }
className = 'w-max p-1 border border-slate-400'
/>
</ section >
< button type = 'submit' > Sign in </ button >
</ form >
) ;
}
Import it to your src/app/auth/page.tsx
file:
src/app/auth/page.tsx import { authOptions } from '@/libs/auth' ;
import { getServerSession } from 'next-auth' ;
import { redirect } from 'next/navigation' ;
import SigninForm from './form' ;
export default async function Signin () {
const session = await getServerSession (authOptions) ;
if (session) {
return redirect ( '/' ) ;
}
return (
<>
< h1 > Sign in </ h1 >
< SigninForm />
</>
) ;
}
As you can see, you can redirect users if they’re not authenticated getting the
session with getServerSession .
3. Improve your UI
Let’s create a short posts like app.
First, add some shadcn/ui components and update your components, we’ll create
new components too:
Terminal npx shadcn-ui@latest add button
Terminal npx shadcn-ui@latest add dialog
Terminal npx shadcn-ui@latest add input
Terminal npx shadcn-ui@latest add textarea
Terminal npx shadcn-ui@latest add form
Terminal npx shadcn-ui@latest add label
Terminal npx shadcn-ui@latest add sonner
We’ll add the endpoints URL for these components, but we’ll create them later.
src/app/auth/signin-form.tsx
Here we’ll update the UI and add form validation.
src/app/auth/signin-form.tsx 'use client' ;
import { signIn } from 'next-auth/react' ;
import { Button } from '@/components/ui/button' ;
import {
Form ,
FormControl ,
FormField ,
FormItem ,
FormLabel ,
FormMessage ,
} from '@/components/ui/form' ;
import { Input } from '@/components/ui/input' ;
import { Textarea } from '@/components/ui/textarea' ;
import { useForm } from 'react-hook-form' ;
import * as z from 'zod' ;
import { zodResolver } from '@hookform/resolvers/zod' ;
const formSchema = z . object ( {
email : z . string () . email () ,
} ) ;
export default function SigninForm () {
const form = useForm < z . infer < typeof formSchema >> ( {
resolver : zodResolver (formSchema) ,
defaultValues : {
email : '' ,
},
} ) ;
async function onSubmit ({ email } : z . infer < typeof formSchema > ) {
await signIn ( 'email' , {
email ,
callbackUrl : ` ${ window . location . origin } ` ,
} ) ;
}
return (
< Form { ... form } >
< form className = 'space-y-4' onSubmit = { form . handleSubmit (onSubmit) } >
< FormField
control = { form . control }
name = 'email'
render = {({ field }) => (
< FormItem >
< FormLabel > Email </ FormLabel >
< FormControl >
< Input placeholder = 'address@example.com' { ... field } />
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< Button className = 'w-full' type = 'submit' >
Send magic link
</ Button >
</ form >
</ Form >
) ;
}
src/components/post/create.tsx
Update UI components and form validation.
src/components/post/create.tsx 'use client' ;
import { useState } from 'react' ;
import {
Dialog ,
DialogContent ,
DialogDescription ,
DialogHeader ,
DialogTitle ,
DialogTrigger ,
} from '@/components/ui/dialog' ;
import { Button } from '@/components/ui/button' ;
import {
Form ,
FormControl ,
FormField ,
FormItem ,
FormLabel ,
FormMessage ,
} from '@/components/ui/form' ;
import { Input } from '@/components/ui/input' ;
import { Textarea } from '@/components/ui/textarea' ;
import { useForm } from 'react-hook-form' ;
import * as z from 'zod' ;
import { zodResolver } from '@hookform/resolvers/zod' ;
import { useRouter } from 'next/navigation' ;
import { toast } from 'sonner' ;
import { SessionProps } from './types' ;
const formSchema = z . object ( {
title : z . string () . min ( 1 ) . max ( 100 ) ,
content : z . string () . min ( 1 ) ,
} ) ;
export default function CreatePost ( props : SessionProps ) {
const [ open , setOpen ] = useState ( false ) ;
const router = useRouter () ;
const form = useForm < z . infer < typeof formSchema >> ( {
resolver : zodResolver (formSchema) ,
defaultValues : {
title : '' ,
content : '' ,
},
} ) ;
async function onSubmit ( values : z . infer < typeof formSchema > ) {
try {
const res = await fetch ( '/api/posts' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( {
... values ,
authorId : props . session . user ?. id ,
} ) ,
} ) ;
const json = await res . json () ;
if ( ! res . ok) {
toast (json . message) ;
return ;
}
toast ( 'Post created!' ) ;
form . reset () ;
setOpen ( false ) ;
router . refresh () ;
} catch (error) {
console . error (error) ;
}
}
return (
< Dialog open = { open } onOpenChange = { setOpen } >
< DialogTrigger asChild >
< Button > Create post </ Button >
</ DialogTrigger >
< DialogContent className = 'max-w-[300px]' >
< DialogHeader className = 'text-left' >
< DialogTitle > Create post </ DialogTitle >
< DialogDescription >
Please < strong > do not </ strong > post < strong > NSFW </ strong > content.
</ DialogDescription >
</ DialogHeader >
< Form { ... form } >
< form className = 'space-y-4' onSubmit = { form . handleSubmit (onSubmit) } >
< FormField
control = { form . control }
name = 'title'
render = {({ field }) => (
< FormItem >
< FormLabel > Title </ FormLabel >
< FormControl >
< Input placeholder = 'Hi there!' { ... field } />
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = 'content'
render = {({ field }) => (
< FormItem >
< FormLabel > Content </ FormLabel >
< FormControl >
< Textarea
placeholder = 'Testing this great app!'
{ ... field }
/>
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< Button className = 'w-full' type = 'submit' >
Post
</ Button >
</ form >
</ Form >
</ DialogContent >
</ Dialog >
) ;
}
src/app/page.tsx
Fetch data from Prisma, as this page is a server component, we can fetch it
directly.
src/app/page.tsx import { authOptions } from '@/libs/auth' ;
import { getServerSession } from 'next-auth' ;
import prisma from '@/libs/db' ;
import CreatePost from '@/components/post/create' ;
import Post from '@/components/post' ;
export default async function Home () {
const session = await getServerSession (authOptions) ;
// You can fetch data to Prisma in server components
const posts = await prisma . post . findMany ( {
include : {
author : true ,
},
} ) ;
return (
<>
< h1 className = 'mb-5 font-bold text-xl' > Home </ h1 >
{ session ? (
<>
< CreatePost session = { session } />
</>
) : (
<>
< p > You are not logged in </ p >
</>
) }
< h3 className = 'text-lg font-semibold mt-10' > Posts </ h3 >
< ul className = 'mt-5 space-y-2.5' >
{ posts . length > 0 ? (
posts . map ( ( post ) => (
< li key = { post . id } >
< Post { ... post } session = { session } />
</ li >
))
) : (
< p > No posts </ p >
) }
</ ul >
</>
) ;
}
src/components/post/item.tsx
src/components/post/item.tsx 'use client' ;
import DeletePost from './delete' ;
import EditPost from './edit' ;
import { TPostProps } from './types' ;
export default function PostItem ( props : TPostProps ) {
return (
< article className = 'w-max p-2 border border-slate-500 rounded-md' >
< header className = 'flex justify-between items-center' >
< h2 className = 'font-bold text-lg' > { props . title } </ h2 >
{ props . session ?. user ?. id === props . authorId && (
< section className = 'space-x-2' >
< EditPost { ... props } />
< DeletePost { ... props } />
</ section >
) }
</ header >
< p > { props . content } </ p >
< span className = 'text-sm' >
Posted by { props . author ?. email || 'anon' } at { ' ' }
{ new Date (props . createdAt) . toLocaleString () }
</ span >
</ article >
) ;
}
src/components/post/edit.tsx
Create a button icon for opening a dialog rendering the post data for editing,
add validation and fetch to the API endpoint.
src/components/post/edit.tsx 'use client' ;
import { useState } from 'react' ;
import { Edit } from 'lucide-react' ;
import {
Dialog ,
DialogClose ,
DialogContent ,
DialogDescription ,
DialogHeader ,
DialogTitle ,
DialogTrigger ,
} from '@/components/ui/dialog' ;
import {
Form ,
FormControl ,
FormField ,
FormItem ,
FormLabel ,
FormMessage ,
} from '@/components/ui/form' ;
import { Input } from '@/components/ui/input' ;
import { Textarea } from '@/components/ui/textarea' ;
import { useForm } from 'react-hook-form' ;
import * as z from 'zod' ;
import { zodResolver } from '@hookform/resolvers/zod' ;
import { useRouter } from 'next/navigation' ;
import { toast } from 'sonner' ;
import { Button } from '@/components/ui/button' ;
import { TPostProps } from './types' ;
const formSchema = z . object ( {
title : z . string () . min ( 1 ) . max ( 100 ) ,
content : z . string () . min ( 1 ) ,
} ) ;
export default function EditPost ( props : TPostProps ) {
const [ open , setOpen ] = useState ( false ) ;
const router = useRouter () ;
const form = useForm < z . infer < typeof formSchema >> ( {
resolver : zodResolver (formSchema) ,
defaultValues : {
title : props . title ,
content : props . content ,
},
} ) ;
async function onSubmit ( values : z . infer < typeof formSchema > ) {
try {
const res = await fetch ( '/api/posts' , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( {
... values ,
id : props . id ,
} ) ,
} ) ;
const json = await res . json () ;
if ( ! res . ok) {
toast (json . message) ;
return ;
}
toast ( 'Post edited!' ) ;
form . reset () ;
setOpen ( false ) ;
router . refresh () ;
} catch (error) {
console . error (error) ;
}
}
return (
< Dialog open = { open } onOpenChange = { setOpen } >
< DialogTrigger asChild >
< Button variant = 'secondary' size = 'icon' >
< Edit />
</ Button >
</ DialogTrigger >
< DialogContent className = 'max-w-[300px]' >
< DialogHeader className = 'text-left' >
< DialogTitle > Edit post </ DialogTitle >
< DialogDescription >
Please < strong > do not </ strong > post < strong > NSFW </ strong > content.
</ DialogDescription >
</ DialogHeader >
< Form { ... form } >
< form className = 'space-y-4' onSubmit = { form . handleSubmit (onSubmit) } >
< FormField
control = { form . control }
name = 'title'
render = {({ field }) => (
< FormItem >
< FormLabel > Title </ FormLabel >
< FormControl >
< Input placeholder = 'Hi there!' { ... field } />
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = 'content'
render = {({ field }) => (
< FormItem >
< FormLabel > Content </ FormLabel >
< FormControl >
< Textarea
placeholder = 'Testing this great app!'
{ ... field }
/>
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< DialogClose asChild >
< Button className = 'w-full' type = 'submit' >
Edit post
</ Button >
</ DialogClose >
</ form >
</ Form >
</ DialogContent >
</ Dialog >
) ;
}
src/components/post/delete.tsx
Create a button icon for opening a dialog for deleting the post, add validation
and fetch to the API endpoint.
src/components/post/delete.tsx 'use client' ;
import { LucideTrash2 } from 'lucide-react' ;
import { Button } from '@/components/ui/button' ;
import {
Dialog ,
DialogClose ,
DialogContent ,
DialogDescription ,
DialogHeader ,
DialogTitle ,
DialogTrigger ,
} from '@/components/ui/dialog' ;
import { useRouter } from 'next/navigation' ;
import { toast } from 'sonner' ;
import { TPostProps } from './types' ;
export default function DeletePost ( props : TPostProps ) {
const router = useRouter () ;
async function handleDelete () {
try {
const res = await fetch ( '/api/posts' , {
method : 'DELETE' ,
headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ( {
id : props . id ,
} ) ,
} ) ;
const json = await res . json () ;
if ( ! res . ok) {
toast (json . message) ;
return ;
}
toast ( 'Post deleted!' ) ;
router . refresh () ;
} catch (error) {
console . error (error) ;
}
}
return (
< Dialog >
< DialogTrigger asChild >
< Button variant = 'destructive' size = 'icon' >
< LucideTrash2 />
</ Button >
</ DialogTrigger >
< DialogContent className = 'max-w-[300px]' >
< DialogHeader className = 'text-left' >
< DialogTitle > Delete post </ DialogTitle >
< DialogDescription >
Are you sure you want to < strong > delete </ strong > this post? This
action cannot be undone.
</ DialogDescription >
</ DialogHeader >
< footer className = 'flex flex-col gap-2' >
< DialogClose asChild >
< Button variant = 'secondary' className = 'w-full' >
No, keep post
</ Button >
</ DialogClose >
< DialogClose asChild >
< Button
onClick = { handleDelete }
variant = 'destructive'
className = 'w-full'
>
Yes, delete post
</ Button >
</ DialogClose >
</ footer >
</ DialogContent >
</ Dialog >
) ;
}
src/components/post/types.ts
src/components/post/types.ts type SessionProps = {
session : any ;
};
type TPostProps = {
author : {
id : string ;
name : string | null ;
email : string | null ;
emailVerified : Date | null ;
image : string | null ;
} | null ;
id : string ;
createdAt : Date ;
updatedAt : Date ;
title : string ;
content : string ;
authorId : string | null ;
session : any ;
};
export type { SessionProps , TPostProps };
src/components/sign-out.tsx
A simple sign out button.
src/components/sign-out.tsx 'use client' ;
import { signOut } from 'next-auth/react' ;
import { Button } from './ui/button' ;
export default function SignOut () {
return < Button onClick = {() => signOut () } > Sign out </ Button > ;
}
src/components/navbar.tsx
Render the sign out or sign in button depending if the user is logged in or not.
src/components/navbar.tsx import Link from 'next/link' ;
import { Button } from './ui/button' ;
import { getServerSession } from 'next-auth' ;
import { authOptions } from '@/libs/auth' ;
import SignOut from './sign-out' ;
export default async function Navbar () {
const session = await getServerSession (authOptions) ;
return (
< nav className = 'w-full p-4 border-b flex justify-between items-center' >
< section >
< Button variant = 'link' className = 'px-0 font-semibold text-lg' >
< Link href = '/' > Fullstack app </ Link >
</ Button >
</ section >
< section >
{ session ? (
< SignOut />
) : (
< Button asChild >
< Link href = '/auth' > Sign in </ Link >
</ Button >
) }
</ section >
</ nav >
) ;
}
src/app/layout.tsx
Add your Navbar and Toaster components and some styles.
src/app/layout.tsx import { Inter } from 'next/font/google' ;
import Navbar from '@/components/navbar' ;
import { Toaster } from '@/components/ui/sonner' ;
import './globals.css' ;
const inter = Inter ( { subsets : [ 'latin' ] } ) ;
interface Props extends React . PropsWithChildren {}
export default function RootLayout ( props : Props ) {
return (
< html lang = 'en' >
< body className = { inter . className } >
< Navbar />
< main className = 'px-4 py-8' > { props . children } </ main >
< Toaster />
</ body >
</ html >
) ;
}
4. Add CRUD functionality
Now we can add Create, Read, Update and Delete functionality to our app.
prisma/schema.prisma
Update your Prisma schema adding to User model a relationship with Post model:
prisma/schema.prisma generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env ( "DATABASE_URL" )
}
model Account {
id String @id @default ( cuid ())
userId String
type String
provider String
providerAccountId String
refresh_token String ? @db.Text
access_token String ? @db.Text
expires_at Int ?
token_type String ?
scope String ?
id_token String ? @db.Text
session_state String ?
user User @relation ( fields : [userId], references : [id], onDelete : Cascade )
@@unique ( [provider, providerAccountId] )
}
model Session {
id String @id @default ( cuid ())
sessionToken String @unique
userId String
expires DateTime
user User @relation ( fields : [userId], references : [id], onDelete : Cascade )
}
model User {
id String @id @default ( cuid ())
name String ?
email String ? @unique
emailVerified DateTime ?
image String ?
accounts Account []
sessions Session []
posts Post []
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique ( [identifier, token] )
}
model Post {
id String @id @default ( cuid ())
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
title String
content String
author User @relation ( fields : [authorId], references : [id] )
authorId String
}
Generate a new Prisma migration:
Terminal npx prisma migrate dev --name add-posts
Now, create a src/app/api/posts/route.ts
file with a POST , PUT and
DELETE async functions:
src/app/api/posts/route.ts import prisma from '@/libs/db' ;
import { NextRequest , NextResponse } from 'next/server' ;
export async function POST ( req : Request ) {
try {
if ( ! req . body) {
return NextResponse . json ( {
ok : false ,
status : 400 ,
message : 'Data required' ,
} ) ;
}
const json = await req . json () ;
const res = await prisma . post . create ( {
data : json ,
} ) ;
return NextResponse . json ( {
ok : true ,
status : 201 ,
data : res ,
} ) ;
} catch (error) {
if (error instanceof Error ) {
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : error . message ,
} ) ;
}
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : 'Internal server error' ,
} ) ;
}
}
export async function PUT ( req : NextRequest ) {
try {
const body = await req . json () ;
const res = await prisma . post . update ( {
where : { id : body . id },
data : {
title : body . title ,
content : body . content ,
},
} ) ;
return NextResponse . json ( {
ok : true ,
status : 200 ,
data : res ,
} ) ;
} catch (error) {
if (error instanceof Error ) {
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : error . message ,
} ) ;
}
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : 'Internal server error' ,
} ) ;
}
}
export async function DELETE ( req : NextRequest ) {
try {
const body = await req . json () ;
const res = await prisma . post . delete ( {
where : { id : body . id },
} ) ;
return NextResponse . json ( {
ok : true ,
status : 200 ,
data : res ,
} ) ;
} catch (error) {
if (error instanceof Error ) {
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : error . message ,
} ) ;
}
return NextResponse . json ( {
ok : false ,
status : 500 ,
message : 'Internal server error' ,
} ) ;
}
}
Try creating a post in the home.
The page will refresh and you’ll see the post, as you created it, only you can
edit or delete it.
5. Conclusion
As you can see, create a fullstack app with Next.js and Prisma is really easy.
Of course, it could be improved, adding server side validation for inputs,
adding pagination for posts in the home, etc.
Posted: January 18, 2024