Implementing Server Actions in Next.js

image
·

August 11, 2024

Implementing Server Actions in Next.js

Welcome to the third installment of our series on building a SaaS application using Next.js, Prisma, and Supabase! In this post, we will focus on implementing server actions in Next.js. Server actions allow you to handle server-side logic within your application efficiently. By the end of this post, you’ll know how to create server actions for common tasks and follow best practices for server-side logic.

What are Server Actions?

Server actions in Next.js are essentially API routes that enable you to handle server-side operations such as data fetching, form handling, and more. These actions are defined within the pages/api directory, and each file in this directory maps to an API endpoint.

Setting Up API Routes

Let s start by setting up a basic API route in our Next.js application.

  1. Create a New API Route (pages/api/hello.ts)
1// pages/api/hello.ts 2import type { NextApiRequest, NextApiResponse } from 'next' 3 4export default function handler( 5 req: NextApiRequest, 6 res: NextApiResponse 7) { 8 res.status(200).json({ message: 'Hello, World!' }) 9} 10

In this example, we created a simple API route that responds with a "Hello, World!" message. You can access this route by navigating to /api/hello in your browser.

Implementing CRUD Operations with Prisma

Now, let’s implement server actions for CRUD (Create, Read, Update, Delete) operations using Prisma. We'll assume you have a User model defined in your Prisma schema.

  1. Prisma Schema (prisma/schema.prisma)
1// prisma/schema.prisma 2model User { 3 id Int @id @default(autoincrement()) 4 email String @unique 5 name String? 6 createdAt DateTime @default(now()) 7 updatedAt DateTime @updatedAt 8} 9
  1. Create User API Route (pages/api/users/create.ts)
1// pages/api/users/create.ts 2import type { NextApiRequest, NextApiResponse } from 'next' 3import { PrismaClient } from '@prisma/client' 4 5const prisma = new PrismaClient() 6 7export default async function handler( 8 req: NextApiRequest, 9 res: NextApiResponse 10) { 11 if (req.method === 'POST') { 12 const { email, name } = req.body 13 14 try { 15 const user = await prisma.user.create({ 16 data: { 17 email, 18 name, 19 }, 20 }) 21 res.status(201).json(user) 22 } catch (error) { 23 res.status(500).json({ error: 'Error creating user' }) 24 } 25 } else { 26 res.setHeader('Allow', ['POST']) 27 res.status(405).end(`Method ${req.method} Not Allowed`) 28 } 29} 30
  1. Read Users API Route (pages/api/users/index.ts)
1// pages/api/users/index.ts 2import type { NextApiRequest, NextApiResponse } from 'next' 3import { PrismaClient } from '@prisma/client' 4 5const prisma = new PrismaClient() 6 7export default async function handler( 8 req: NextApiRequest, 9 res: NextApiResponse 10) { 11 if (req.method === 'GET') { 12 try { 13 const users = await prisma.user.findMany() 14 res.status(200).json(users) 15 } catch (error) { 16 res.status(500).json({ error: 'Error fetching users' }) 17 } 18 } else { 19 res.setHeader('Allow', ['GET']) 20 res.status(405).end(`Method ${req.method} Not Allowed`) 21 } 22} 23
  1. Update User API Route (pages/api/users/[id].ts)
1// pages/api/users/[id].ts 2import type { NextApiRequest, NextApiResponse } from 'next' 3import { PrismaClient } from '@prisma/client' 4 5const prisma = new PrismaClient() 6 7export default async function handler( 8 req: NextApiRequest, 9 res: NextApiResponse 10) { 11 const { id } = req.query 12 13 if (req.method === 'PUT') { 14 const { email, name } = req.body 15 16 try { 17 const user = await prisma.user.update({ 18 where: { id: Number(id) }, 19 data: { email, name }, 20 }) 21 res.status(200).json(user) 22 } catch (error) { 23 res.status(500).json({ error: 'Error updating user' }) 24 } 25 } else { 26 res.setHeader('Allow', ['PUT']) 27 res.status(405).end(`Method ${req.method} Not Allowed`) 28 } 29} 30
  1. Delete User API Route (pages/api/users/[id].ts)
1// pages/api/users/[id].ts 2import type { NextApiRequest, NextApiResponse } from 'next' 3import { PrismaClient } from '@prisma/client' 4 5const prisma = new PrismaClient() 6 7export default async function handler( 8 req: NextApiRequest, 9 res: NextApiResponse 10) { 11 const { id } = req.query 12 13 if (req.method === 'DELETE') { 14 try { 15 await prisma.user.delete({ 16 where: { id: Number(id) }, 17 }) 18 res.status(204).end() 19 } catch (error) { 20 res.status(500).json({ error: 'Error deleting user' }) 21 } 22 } else { 23 res.setHeader('Allow', ['DELETE']) 24 res.status(405).end(`Method ${req.method} Not Allowed`) 25 } 26} 27

Best Practices for Server Actions

  1. Validation: Always validate the input data to ensure it meets the expected format and constraints before processing it.
  2. Error Handling: Implement robust error handling to manage unexpected errors gracefully and provide informative responses to the client.
  3. Security: Use authentication and authorization mechanisms to protect sensitive routes and data. Sanitize user inputs to prevent SQL injection and other attacks.
  4. Logging: Add logging to track server-side operations and help with debugging and monitoring.

Integrating Server Actions with the Frontend

Let’s integrate these server actions with our frontend to perform CRUD operations from our React components.

  1. Fetching Users (pages/dashboard.tsx)
1// pages/dashboard.tsx 2import { NextPage } from 'next' 3import { useEffect, useState } from 'react' 4import Head from 'next/head' 5 6interface User { 7 id: number 8 email: string 9 name: string 10} 11 12const Dashboard: NextPage = () => { 13 const [users, setUsers] = useState<User[]>([]) 14 15 useEffect(() => { 16 const fetchUsers = async () => { 17 const response = await fetch('/api/users') 18 const data = await response.json() 19 setUsers(data) 20 } 21 22 fetchUsers() 23 }, []) 24 25 return ( 26 <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100"> 27 <Head> 28 <title>Dashboard | My SaaS App</title> 29 </Head> 30 <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center"> 31 <h1 className="text-6xl font-bold">Dashboard</h1> 32 <div className="mt-6"> 33 {users.map((user) => ( 34 <div key={user.id} className="p-4 bg-white shadow rounded-lg mb-4"> 35 <p>Email: {user.email}</p> 36 <p>Name: {user.name}</p> 37 </div> 38 ))} 39 </div> 40 </main> 41 </div> 42 ) 43} 44 45export default Dashboard 46
  1. Creating a User (pages/auth/register.tsx)
1// pages/auth/register.tsx 2import { NextPage } from 'next' 3import Head from 'next/head' 4import { useState } from 'react' 5import Link from 'next/link' 6 7const Register: NextPage = () => { 8 const [email, setEmail] = useState('') 9 const [name, setName] = useState('') 10 11 const handleRegister = async (event: React.FormEvent) => { 12 event.preventDefault() 13 const response = await fetch('/api/users/create', { 14 method: 'POST', 15 headers: { 16 'Content-Type': 'application/json', 17 }, 18 body: JSON.stringify({ email, name }), 19 }) 20 21 if (response.ok) { 22 console.log('User created successfully') 23 } else { 24 console.error('Error creating user') 25 } 26 } 27 28 return ( 29 <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100"> 30 <Head> 31 <title>Register | My SaaS App</title> 32 </Head> 33 <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center"> 34 <h1 className="text-6xl font-bold">Register</h1> 35 <form className="mt-8 space-y-6" onSubmit={handleRegister}> 36 <div className="rounded-md shadow-sm -space-y-px"> 37 <div> 38 <label htmlFor="email" className="sr-only">Email address</label> 39 <input 40 id="email" 41 name="email" 42 type="email" 43 autoComplete="email" 44 required 45 className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border 46 ```typescript 47 focus:border-indigo-500 focus:z-10 sm:text-sm" 48 placeholder="Email address" 49 value={email} 50 onChange={(e) => setEmail(e.target.value)} 51 /> 52 </div> 53 <div> 54 <label htmlFor="name" className="sr-only">Name</label> 55 <input 56 id="name" 57 name="name" 58 type="text" 59 autoComplete="name" 60 required 61 className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" 62 placeholder="Name" 63 value={name} 64 onChange={(e) => setName(e.target.value)} 65 /> 66 </div> 67 </div> 68 <div> 69 <button 70 type="submit" 71 className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" 72 > 73 Sign up 74 </button> 75 </div> 76 </form> 77 <p className="mt-2 text-sm text-gray-600"> 78 Already have an account?{' '} 79 <Link href="/auth/login"> 80 <a className="font-medium text-indigo-600 hover:text-indigo-500">Sign in</a> 81 </Link> 82 </p> 83 </main> 84 </div> 85 ) 86} 87 88export default Register 89

Updating and Deleting Users

We can similarly create forms and functions to update and delete users, following the same principles as above.

  1. Update User Form

Create a form for updating a user in pages/dashboard.tsx or in a dedicated update page. Use a PUT request to the /api/users/[id] endpoint.

  1. Delete User Function

Add a delete button next to each user in pages/dashboard.tsx and send a DELETE request to the /api/users/[id] endpoint when clicked.

1// pages/dashboard.tsx (additional code) 2const handleDelete = async (id: number) => { 3 const response = await fetch(`/api/users/${id}`, { 4 method: 'DELETE', 5 }) 6 7 if (response.ok) { 8 setUsers(users.filter((user) => user.id !== id)) 9 console.log('User deleted successfully') 10 } else { 11 console.error('Error deleting user') 12 } 13} 14 15// In the return statement, next to each user: 16<button 17 onClick={() => handleDelete(user.id)} 18 className="text-red-600 hover:text-red-800" 19> 20 Delete 21</button> 22

Best Practices for Server Actions

  1. Validation: Always validate the input data to ensure it meets the expected format and constraints before processing it.
  2. Error Handling: Implement robust error handling to manage unexpected errors gracefully and provide informative responses to the client.
  3. Security: Use authentication and authorization mechanisms to protect sensitive routes and data. Sanitize user inputs to prevent SQL injection and other attacks.
  4. Logging: Add logging to track server-side operations and help with debugging and monitoring.

Conclusion

In this post, we have explored how to implement server actions in Next.js using API routes. We created routes for performing CRUD operations with Prisma, integrated them with our frontend, and followed best practices for server-side logic. In the next post, we’ll dive into user authentication and authorization with Supabase. Stay tuned!

By following this series, you'll gain a comprehensive understanding of how to leverage these powerful tools to build a modern, scalable SaaS application. Happy coding!

Let's Build Something Amazing Together

Ready to transform your digital landscape? Contact Codeks today to book a free consultation. Let's embark on a journey to innovation, excellence, and success together.