docs/local-api/server-functions.mdx
In Next.js, server functions (previously called server actions) are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic.
Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints.
All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely.
For a full list of available operations, see the Local API overview.
In the following examples, we'll cover some common use cases, including:
First, let's create our server function. Here are some key points for this process:
'use server' at the top of the file.getPayload().payload.create() and pass in the relevant data.try...catch block for error handling.'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPost(data) {
const payload = await getPayload({ config })
try {
const post = await payload.create({
collection: 'posts',
data,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
Now, let's look at how to call the createPost function we just created from the frontend in a React component when a user clicks a button:
'use client';
import React, { useState } from 'react';
import { createPost } from '../server/actions'; // import the server function
export const PostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function
const newPost = await createPost({ title: 'Sample Post' });
setResult('Post created: ' + newPost.title);
}}
>
Create Post
</button>
</>
);
};
The key points from the previous example also apply here.
To update a document instead of creating one, you would use payload.update() with the relevant data and passing the document ID.
Here's how the server function would look:
'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function updatePost(id, data) {
const payload = await getPayload({ config })
try {
const post = await payload.update({
collection: 'posts',
id, // the document id is required
data,
})
return post
} catch (error) {
throw new Error(`Error updating post: ${error.message}`)
}
}
Here is how you would call the updatePost function from a frontend React component:
'use client';
import React, { useState } from 'react';
import { updatePost } from '../server/actions'; // import the server function
export const UpdatePostForm: React.FC = () => {
const [result, setResult] = useState<string>('');
return (
<>
<p>{result}</p>
<button
type="button"
onClick={async () => {
// Call the server function to update the post
const updatedPost = await updatePost('your-post-id-123', { title: 'Updated Post' });
setResult('Post updated: ' + updatedPost.title);
}}
>
Update Post
</button>
</>
);
};
In this example, we will check if a user is authenticated using Payload's authentication system. Here's how it works:
next/headers to retrieve the request headers.payload.auth() to fetch the user's authentication details.Here's the server function to authenticate a user:
'use server'
import { headers as getHeaders } from 'next/headers'
import config from '@payload-config'
import { getPayload } from 'payload'
export const authenticateUser = async () => {
const payload = await getPayload({ config })
const headers = await getHeaders()
const { user } = await payload.auth({ headers })
if (user) {
return { hello: user.email }
}
return { hello: 'Not authenticated' }
}
Here's a basic example of how to call the authentication server function from the frontend to test it:
'use client';
import React, { useState } from 'react';
import { authenticateUser } from '../server/actions'; // Import the server function
export const AuthComponent: React.FC = () => {
const [userInfo, setUserInfo] = useState<string>('');
return (
<React.Fragment>
<p>{userInfo}</p>
<button
onClick={async () => {
// Call the server function to authenticate the user
const result = await authenticateUser();
setUserInfo(result.hello);
}}
type="button"
>
Check Authentication
</button>
</React.Fragment>
);
};
This example demonstrates how to write a server function that creates a document with a file upload. Here are the key steps:
payload.create() to create a new post document with both the document data and file'use server'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function createPostWithUpload(data, upload) {
const payload = await getPayload({ config })
try {
// Prepare the data with the file
const postData = {
...data,
media: upload,
}
const post = await payload.create({
collection: 'posts',
data: postData,
})
return post
} catch (error) {
throw new Error(`Error creating post: ${error.message}`)
}
}
Here is how you would use the server function we just created in a frontend component to allow users to submit a post along with a file upload:
handleSubmit function checks if a file has been chosen.createPostWithFile server function.'use client';
import React, { useState } from 'react';
import { createPostWithUpload } from '../server/actions';
export const PostForm: React.FC = () => {
const [title, setTitle] = useState<string>('');
const [file, setFile] = useState<File | null>(null);
const [result, setResult] = useState<string>('');
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
setResult('Please upload a file.');
return;
}
try {
// Call the server function to create the post with the file
const newPost = await createPostWithUpload({ title }, file);
setResult('Post created with file: ' + newPost.title);
} catch (error) {
setResult('Error: ' + error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post Title"
/>
<input type="file" onChange={handleFileChange} />
<button type="submit">Create Post</button>
<p>{result}</p>
</form>
);
};
Managing authentication with the Local API can be tricky as you have to handle cookies and tokens yourself, and there aren't built-in logout or refresh functions since these only modify cookies. To make this easier, we provide login, logout, and refresh as ready-to-use server functions. They take care of the underlying complexity so you don't have to.
Logs in a user by verifying credentials and setting the authentication cookie. This function allows login via username or email, depending on the collection auth configuration.
login functionimport { login } from '@payloadcms/next/auth'
The login function needs your Payload config, which cannot be imported in a client component. To work around this, create a simple server function like the one below, and call it from your client.
'use server'
import { login } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function loginAction({
email,
password,
}: {
email: string
password: string
}) {
try {
const result = await login({
collection: 'users',
config,
email,
password,
})
return result
} catch (error) {
throw new Error(
`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
'use client'
import { useState } from 'react'
import { loginAction } from '../loginAction'
export default function LoginForm() {
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
return (
<form onSubmit={() => loginAction({ email, password })}>
<label htmlFor="email">Email</label>
<input
id="email"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
type="email"
value={email}
/>
<label htmlFor="password">Password</label>
<input
id="password"
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
type="password"
value={password}
/>
<button type="submit">Login</button>
</form>
)
}
Logs out the current user by clearing the authentication cookie and current sessions.
logout functionimport { logout } from '@payloadcms/next/auth'
Similar to the login function, you now need to pass your Payload config to this function and this cannot be done in a client component. Use a helper server function as shown below. To ensure all sessions are cleared, set allSessions: true in the options, if you wish to logout but keep current sessions active, you can set this to false or leave it undefined.
'use server'
import { logout } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function logoutAction() {
try {
return await logout({ allSessions: true, config })
} catch (error) {
throw new Error(
`Logout failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
'use client'
import { logoutAction } from '../logoutAction'
export default function LogoutButton() {
return <button onClick={() => logoutFunction()}>Logout</button>
}
Refreshes the authentication token and current session for the logged-in user.
refresh functionimport { refresh } from '@payloadcms/next/auth'
As with login and logout, you need to pass your Payload config to this function. Create a helper server function like the one below. Passing the config directly to the client is not possible and will throw errors.
'use server'
import { refresh } from '@payloadcms/next/auth'
import config from '@payload-config'
export async function refreshAction() {
try {
return await refresh({
config,
})
} catch (error) {
throw new Error(
`Refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
}
}
'use client'
import { refreshAction } from '../actions/refreshAction'
export default function RefreshTokenButton() {
return <button onClick={() => refreshFunction()}>Refresh</button>
}
When using server functions, proper error handling is essential to prevent unhandled exceptions and provide meaningful feedback to the frontend.
Example of good error handling:
export async function createPost(data) {
try {
const payload = await getPayload({ config })
return await payload.create({ collection: 'posts', data })
} catch (error) {
console.error('Error creating post:', error)
return { error: 'Failed to create post' }
}
}
Using server functions helps prevent direct exposure of Local API operations to the frontend, but additional security best practices should be followed:
Example of restricting access based on user role:
import { UnauthorizedError } from 'payload'
export async function deletePost(postId, user) {
if (!user || user.role !== 'admin') {
throw new UnauthorizedError()
}
const payload = await getPayload({ config })
return await payload.delete({ collection: 'posts', id: postId })
}