Construye una App Fullstack
Tech stack usado en este tutorial
GitHub repo
Contenido
Introducción
Configuración inicial
Instalar shadcn/ui
Crear una base de datos PostgreSQL usando Docker
Instalar Prisma
Configurar Auth.js
Mejora tu UI
Añade funcionalidad CRUD
Conclusión
1. Introducción
En este tutorial, desarrollaremos una app fullstack con el siguiente tech stack:
Aprenderemos algunos de los fundamentos de este tech stack, como usar server components en Next.js, o crear endpoints API usando el app router .
2. Configuración inicial
Comencemos creando un nuevo proyecto Next.js, en tu terminal
ejecuta:
Terminal npx create-next-app@latest
Asegúrate de marcar Yes
en las siguientes opciones:
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 ? @/ *
Espera a que la instalación de las dependencias termine, luego accede a la carpeta del proyecto:
Terminal cd fullstack-app
Abre tu editor de código de preferencia.
2.1 Instalar shadcn/ui
Estos componentes nos ayudarán mucho a construir la UI junto con TailwindCSS.
Primero, inicializa shadcn/ui:
Terminal npx shadcn-ui@latest init
Asegúrate de configurar shadcn/ui de acuerdo a tu proyecto.
Puedes revisar la documentación de shadcn/ui para cada componente que puedas necesitar, cada componente se instala individualmente.
2.2 Crear una base de datos PostgreSQL usando Docker
Asegúrate de tener Docker instalado en tu máquina.
Primero, necesitas descargar una imagen de PostgreSQL de Docker Hub:
Terminal docker pull postgres
Luego, crea un contenedor con la imagen:
Terminal docker run --name my-postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres
2.3 Instalar Prisma
Instala Prisma usando tu gestor de dependencias, en este caso npm :
Terminal npm install prisma -D
Ahora inicializa Prisma:
Terminal npx prisma init
Se creará un directorio ./prisma
en la raíz de tu proyecto, con un archivo schema.prisma
.
Aquí crearás tus schemas.
Agrega este modelo como ejemplo:
prisma/schema.prisma model User {
id Int @id @default ( autoincrement ())
createdAt DateTime @default ( now ())
email String @unique
name String ?
}
Actualiza tu archivo .env con la siguiente URL:
.env DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?schema=public"
En esta URL está tu nombre de usuario (por defecto es postgres), tu contraseña (en este caso password), el host (por defecto es localhost), el puerto (por defecto es 5432), el nombre de la base de datos (por defecto es postgres) y el schema (por defecto es public).
Crea tu primera migración para probar si Prisma puede conectarse a tu base de datos local:
Terminal npx prisma migrate dev --name init
Si todo está bien, verás un nuevo directorio /migrations
con un nuevo archivo dentro.
Si tienes un error, asegúrate de poder conectarte a tu base de datos local. Elimina y crea el contenedor de nuevo si es necesario.
2.4 Configurar Auth.js
Agrega este modelo a tu schema.prisma
:
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] )
}
Estos modelos son para Auth.js , ahora podemos instalarlo con el adaptador de Prisma:
Terminal npm install @prisma/client @auth/prisma-adapter
Instalaremos nodemailer también, ya que usaremos magic links para la autenticación:
Terminal npm install nodemailer -D
Ahora crea src/utils/db.ts
e inicializa 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 ;
Luego, crea src/libs/auth.ts
para configurar 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 ;
Esta configuración es para usar un proveedor de email, para este proyecto usaremos Resend .
Crea una cuenta y luego obtén las siguientes credenciales en tu archivo .env:
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
Ahora, crea un archivo src/pages/api/auth/[...nextauth].ts
:
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 };
Este archivo es para manejar la autenticación en nuestra app.
Ahora puedes autenticar usuarios con un magic link enviado por email.
Crea un archivo src/app/auth/signin-form.tsx
:
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 >
) ;
}
Impórtalo en src/app/auth/page.tsx
:
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 />
</>
) ;
}
Como puedes ver, puedes redirigir a los usuarios si no están autenticados obteniendo la sesión con getServerSession .
3. Mejora tu UI
Creemos una app para posts cortos.
Primero, agrega algunos componentes de shadcn/ui y actualiza tus componentes, también crearemos nuevos componentes:
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
Agregaremos las URL de los endpoints para estos componentes, pero más adelante.
src/app/auth/signin-form.tsx
Aquí actualizaremos la UI y agregaremos validación de formulario.
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
Actualiza los componentes de UI y agrega validación de formulario.
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
Haz fetch de los dats con Prisma, como esta página es un server component, podemos hacer fetch directamente.
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
Crea un botón para abrir un diálogo que renderice los datos del post para editar, agrega validación y fetch al endpoint de la API.
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
Crea un botón para abrir un diálogo para eliminar el post, agrega validación y fetch al endpoint de la API.
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
Un botón simple para cerrar sesión.
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
Renderiza el botón de cerrar sesión o iniciar sesión dependiendo si el usuario está autenticado o no.
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
Agrega tu Navbar y Toaster y algunos estilos.
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. Añade funcionalidad CRUD
Ahora podemos crear, leer, actualizar y eliminar posts.
prisma/schema.prisma
Actualiza tu schema de Prisma con el modelo Post:
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
}
Genera una nueva migración:
Terminal npx prisma migrate dev --name add-posts
Ahora, create un archivo src/app/api/posts/route.ts
con las funciones asíncronas GET , PUT y DELETE :
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' ,
} ) ;
}
}
Intenta crear un post en el home.
La página se refrescará y verás el post que creaste, solo tú puedes editarlo o eliminarlo.
5. Conclusión
Como puedes ver, crear una app fullstack con Next.js es muy fácil.
Por supuesto, se puede mejorar, agregando validación del lado del servidor para los inputs, agregando paginación para los posts en el home, etc.
Si quieres que trabajemos juntos, envíame un correo a contact@juancman.dev .
Publicado: 18 de Enero del 2024