Building Real-time Apps with Next.js 13.4 Server Actionsby@leandronnz
17,627 reads
17,627 reads

Building Real-time Apps with Next.js 13.4 Server Actions

by Leandro NuñezAugust 10th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

The article explores building real-time web applications using Next.js 13.4's server actions. It explains real-time application concepts, introduces Next.js 13.4's server actions, covers setting up and creating server actions, discusses designing frontends for seamless user experiences, offers testing strategies, optimization techniques, and future enhancements. The article provides code examples, guidelines, and resources for building effective real-time applications.
featured image - Building Real-time Apps with Next.js 13.4 Server Actions
Leandro Nuñez HackerNoon profile picture

Hello there!

If you're anything like me, you've probably found yourself marveling at the seamless interactivity of today's real-time web applications—those chatbots that respond instantly, the live notifications that pop up without a page refresh, and collaborative tools that update in the blink of an eye. Real-time functionality has become less of a luxury and more of an expectation in the digital age.

Now, if you've been tracking the developments in the world of Next.js, you might've caught wind of the buzzworthy features of version 13.4, especially the game-changer: server actions. Are you curious about how this can redefine the way we craft real-time experiences?

Well, so was I!

Dive with me into this case study, where we'll embark on a journey to construct a real-time application, leveraging the power and finesse of Next.js server actions. Whether you're a seasoned developer or just venturing into the realm of real-time apps, there's a trove of insights waiting for you.

Let’s get the ball rolling, shall we?

Table of Contents

  1. Background: Understanding Real-time Applications
  2. What’s New in Next.js 13.4: A Glimpse at Server Actions
  3. Setting the Stage: Our Real-time Project Scope
  4. Getting Started: Initial Setup
  5. Building the Backend: Leveraging Server Actions
  6. Designing the Frontend: A Seamless User Experience
  7. Testing the Real-time Capabilities
  8. Enhancements and Optimizations
  9. Conclusion and Future Prospects
  10. Resources and Further Reading

1. Background: Understanding Real-time Applications

In today's fast-paced digital landscape, the term "real-time" often pops up across various contexts—from gaming and finance to communication and social media. But what exactly does "real-time" mean in the world of web applications?

Let's demystify this.

What are Real-time Applications?

Real-time applications are systems or programs that immediately respond to user inputs or external events, offering instant feedback without perceptible delays. In simpler terms, think of them as live, dynamic platforms that evolve in "real-time," mirroring the constant flow of information in the modern digital ecosystem.

Real-life Examples

To put it into perspective, consider some ubiquitous examples:

  • Instant Messaging Apps: Platforms like WhatsApp and Telegram where messages are sent, received, and seen without delay.

  • Collaborative Tools: Think of Google Docs, where multiple users can edit a document simultaneously, observing each other's changes in real-time.

  • Live Stock Tickers: Platforms that display stock prices that update instantaneously with market fluctuations.

  • Online Multiplayer Games: Where players interact with each other and the environment with zero latency, ensuring a seamless gaming experience.

The Relevance of Real-time Applications

So, why is real-time functionality so sought after?

  • User Expectation: Modern users expect immediacy. Whether it's a chat application or a weather update, any noticeable lag can lead to decreased user satisfaction.
  • Enhanced Interactivity: Real-time features enable a more interactive and immersive user experience, promoting user engagement.
  • Competitive Advantage: Offering real-time features can set platforms apart in a crowded market, offering a unique selling point that attracts and retains users.

The Challenges Ahead

Building real-time applications is not without its hurdles:

  • Scalability Issues: Real-time apps often need to handle numerous simultaneous connections, requiring robust infrastructure.

  • Data Integrity: Ensuring that real-time data remains consistent across various user interfaces can be a challenge, especially with multiple simultaneous edits or interactions.

  • Latency: A real-time app is only as good as its slowest component. Ensuring minimal delays requires careful optimization and efficient use of resources.

Now that we've set the stage with a foundational understanding of real-time applications, we'll delve into how Next.js 13.4, with its server actions, emerges as a pivotal tool for developers aspiring to craft such immersive experiences.

2. What’s New in Next.js 13.4: A Glimpse at Server Actions

In the ever-evolving landscape of web development, Next.js has consistently been at the forefront, introducing features that redefine how we approach building applications. Version 13.4 is no exception, particularly with its emphasis on server actions. But before we dive deep, let's clarify some terminology:

A Primer on Actions

Actions in the React ecosystem, although still experimental, have brought about a paradigm shift by allowing developers to execute asynchronous code in response to user interactions.

Interestingly, while they aren't exclusive to Next.js or React Server Components, their use through Next.js means you're on the React experimental channel.

For those familiar with HTML forms, you might recall passing URLs to the action prop. Now, with Actions, you can directly pass a function, making interactions more dynamic and integrated.

<button action={() => { /* async function logic here */ }}>Click me!</button>

React's integration with Actions also offers built-in solutions for optimistic updates. This emphasizes that while Actions are groundbreaking, the patterns are still evolving, and newer APIs might be added to further enrich them.

Embracing Form Actions

Form Actions represent an ingenious amalgamation of React's Actions with the standard <form> API. They resonate with the primitive formaction attribute in HTML, making it possible for developers to enhance progressive loading states and other functionalities out-of-the-box.

<!-- Traditional HTML approach -->
<form action="/submit-url">
   <!-- form elements -->

<!-- With Next.js 13.4 Form Actions -->
<form action={asyncFunctionForSubmission}>
   <!-- form elements -->

Server Functions & Server Actions

Server Functions are essentially functions that operate on the server-side but can be invoked from the client. These elevate Next.js's server-side rendering capabilities to a whole new level.

Transitioning to Server Actions, they can be understood as Server Functions, but ones specifically triggered as an action. Their integration with form elements, especially through the action prop, ensures that the form remains interactive even before the client-side JavaScript loads. This translates to a smoother user experience, with React hydration not being a prerequisite for form submission.

// A simple Server Action in Next.js 13.4
<form action={serverActionFunction}>
   <!-- form elements -->

Understanding Server Mutations

Lastly, we have Server Mutations, which are a subset of Server Actions. These are particularly powerful when you need to modify data on the server and then execute specific responses, such as redirect, revalidatePath, or revalidateTag.

const serverMutationFunction = async () => {
    // Modify data logic here...
    // ...
    return { revalidatePath: '/updated-path' };

<form action={serverMutationFunction}>
   <!-- form elements -->

Notes: In summary, Next.js 13.4's Server Actions framework, underpinned by Actions, Form Actions, Server Functions, and Server Mutations, embodies a transformative approach to real-time web applications.

As we move forward in our case study, you'll witness firsthand the prowess these features bring to the table.

So, let's gear up for the exciting journey ahead!

3. Setting the Stage: Our Real-time Project Scope

In the context of building a real-time application, Next.js 13.4's Server Actions play a crucial role. These alpha features make it easy to manage server-side data mutations, reduce client-side JavaScript, and progressively enhance forms.

Enabling Server Actions

First, you'll need to enable Server Actions in your Next.js project. Simply add the following code to your next.config.js file:

module.exports = {
  experimental: {
    serverActions: true,

Creation and Invocation

Server Actions can be defined either within the Server Component that uses it or in a separate file for reusability between Client and Server Components.

Here’s how you can create and invoke Server Actions:

  1. Within Server Components: A Server Action can be easily defined within a Server Component, like this:

    export default function ServerComponent() {
      async function myAction() {
        'use server'
        // ...

  2. With Client Components: When using a Server Action inside a Client Component, create the action in a separate file and then import it.

    // app/actions.js
    'use server'
    export async function myAction() {
      // ...

  3. Importing and using in Client Component:

    // app/client-component.js
    import { myAction } from './actions'
    export default function ClientComponent() {
      return (
        <form action={myAction}>
          <button type="submit">Add to Cart</button>

  4. Custom Invocation: You can use custom methods like startTransition to invoke Server Actions outside of forms, buttons, or inputs.

    // Example using startTransition
    'use client'
    import { useTransition } from 'react'
    import { addItem } from '../actions'
    function ExampleClientComponent({ id }) {
      let [isPending, startTransition] = useTransition()
      return (
        <button onClick={() => startTransition(() => addItem(id))}>
          Add To Cart

Progressive Enhancement

Next.js 13.4 also offers Progressive Enhancement, allowing a <form> to function without JavaScript. Server Actions can be passed directly to a <form>, making the form interactive even if JavaScript is disabled.

// app/components/example-client-component.js
'use client'
import { handleSubmit } from './actions.js'

export default function ExampleClientComponent({ myAction }) {
  return (
    <form action={handleSubmit}>
      {/* ... */}

Size Limitation

The maximum request body sent to a Server Action is 1MB by default. If needed, you can configure this limit using the serverActionsBodySizeLimit option:

module.exports = {
  experimental: {
    serverActions: true,
    serverActionsBodySizeLimit: '2mb',

4. Getting Started: Initial Setup

Creating a new Next.js 13.4 project

To get started with building a real-time application using Next.js 13.4, the first step is to create a new project. You can use the standard Next.js CLI command to initialize your project:

npx create-next-app@latest my-real-time-app

Replace my-real-time-app with the desired name for your project. This command sets up a new Next.js project with default configurations.

Required dependencies and packages for real-time functionality

For real-time functionality, there are certain packages and dependencies you may require. Depending on the specifics of your application, these could range from WebSockets libraries to GraphQL subscriptions and more.

Ensure you've reviewed the project requirements and added the necessary dependencies.

However, with Next.js 13.4's support for Server Actions, there's already a built-in setup that supports server-side processing, which can assist in achieving some of the real-time features.

A brief overview of the project structure and directory setup

The App Router

With the introduction of Next.js 13.4, the App Router is a significant feature that allows developers to utilize shared layouts, nested routing, error handling, and more. It's designed to work in conjunction with the existing pages directory, but it's housed within a new directory named app.

To get started with the App Router:

  1. Create an app directory in the root of your project.

  2. Add your routes or components inside this directory.

By default, components inside the app directory are Server Components, offering optimal performance and allowing developers to easily adopt them.

Here's an example structure:

├── app/                          # Main directory for App Router components
│   ├── _error.js                 # Custom error page
│   ├── _layout.js                # Shared layout for the app
│   │
│   ├── dashboard/               # Nested route example
│   │   ├── index.js             # Dashboard main view
│   │   └── settings.js          # Dashboard settings view
│   │
│   ├── index.js                  # Landing/Home page
│   ├── profile.js                # User profile page
│   ├── login.js                  # Login page
│   └── register.js               # Registration page
├── public/                       # Static assets go here (images, fonts, etc.)
│   ├── images/
│   └── favicon.ico
├── styles/                      # Global styles or variables
│   └── global.css
├── package.json                  # Dependencies and scripts
├── next.config.js                # Next.js configuration
└──                     # Project documentation

Server Components vs. Client Components

Thinking about how components render is crucial. In traditional SPAs (Single Page Applications), React renders the entire application on the client side. With Server Components, much of the application renders on the server, leading to performance benefits. Here's a guideline:

  • Server Components: Ideal for non-interactive parts of your application. These components are rendered on the server and sent to the client as HTML. The advantage here is improved performance, reduced client-side JavaScript, and the ability to fetch data or access backend resources directly.

  • Client Components: Used for interactive UI elements. They're pre-rendered on the server and then "hydrated" on the client to add interactivity.

To differentiate between these components, Next.js introduced the "use client" directive. This directive indicates that a component should be treated as a Client Component. It should be placed at the top of a component file, before any imports.

For example, if you have an interactive counter, as in the provided code, you'll use the "use client" directive to indicate that it's a client-side component.


As you structure your application, here are some guidelines:

  1. Use Server Components by default (as they are in the app directory).

  2. Only opt for Client Components when you have specific use cases like adding interactivity, utilizing browser-only APIs, or leveraging React hooks that depend on state or browser functionalities.

Notes: Following this structure and setup, you'll be well on your way to building a performant real-time application with Next.js 13.4's Server Actions.

5. Building the Backend: Leveraging Server Actions

The power of Next.js 13.4 shines when integrating real-time backend functionalities into our project. Let's walk through the steps with relevant code examples for our my-real-time-app.

Introduction to how server actions will be employed in this project

For our my-real-time-app, server actions act as our primary bridge between the frontend and backend, allowing for efficient data transactions without the need for separate APIs.

// my-real-time-app/app/actions/index.js

export * from './auth-action';
export * from './chat-action';

Setting up server actions for handling user authentication

In my-real-time-app, we leverage server actions to streamline the authentication process.

// my-real-time-app/app/actions/auth-action.js

export const login = async (credentials) => {
  // Logic for authenticating user with credentials
  // Return user details or error message

export const logout = async (userId) => {
  // Logic for logging out the user
  // Return success or error message

export const register = async (userInfo) => {
  // Logic for registering a new user
  // Store user in database and return success or error message

Creating server actions for sending and receiving real-time messages

For the chat functionality:

// my-real-time-app/app/actions/chat-action.js

export const sendMessage = async (messageDetails) => {
  // Logic to send a new message
  // Store message in database and inform other users via WebSocket or similar

export const receiveMessage = async () => {
  // Logic to receive a message in real-time
  // Return the message details

export const getRecentMessages = async (userId) => {
  // Logic to fetch recent messages for the user
  // Retrieve messages from the database

Integrating a database (e.g., MongoDB) for message persistence

Using MongoDB as our primary data store:

// Initialize MongoDB connection
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();

// Now, use this connection in server actions to interact with the database.

In our chat actions:

// my-real-time-app/app/actions/chat-action.js

export const sendMessage = async (messageDetails) => {
  const messagesCollection = client.db('chatDB').collection('messages');
  await messagesCollection.insertOne(messageDetails);
  // Inform other users via WebSocket or similar

Ensuring secure and fast communication through server actions

For security:

// Middleware for validating request data
const validateRequest = (req) => {
  // Validation logic here
  return isValid;

export const sendMessage = async (messageDetails) => {
  if (!validateRequest(messageDetails)) {
    throw new Error("Invalid request data");
  // Remaining logic...

6. Designing the Frontend: A Seamless User Experience

In this section, we'll construct an intuitive and responsive chat interface for my-real-time-app. The integration of Next.js 13.4's server components will enable real-time updates for a smooth user experience.

Architecting the main chat interface

Firstly, let's create the main chat interface:

// my-real-time-app/app/chat-interface.js

import { useEffect, useState } from 'react';
import { getRecentMessages } from './actions/chat-action';

export default function ChatInterface() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    async function loadMessages() {
      const recentMessages = await getRecentMessages();

  }, []);

  return (
    <div className="chatBox">
      { => (
        <p key={}>{msg.content}</p>

This component fetches recent messages on load and renders them in a chatbox.

Connecting the frontend to server actions for real-time updates

Now, we'll set up real-time updates using a basic example of WebSockets:

// my-real-time-app/app/chat-interface.js

const [socket, setSocket] = useState(null);

useEffect(() => {
  const ws = new WebSocket("ws://your-backend-url/ws");

  ws.onmessage = (event) => {
    const newMessage = JSON.parse(;
    setMessages(prevMessages => [...prevMessages, newMessage]);

  return () => {
}, []);

This hook establishes a WebSocket connection and updates the message list when a new message is received.

Implementing notifications for new messages

For a better UX, let's notify users of new messages:

// my-real-time-app/app/chat-interface.js

useEffect(() => {
  if (messages.length && "Notification" in window && Notification.permission === "granted") {
    const lastMessage = messages[messages.length - 1];
    new Notification(`New message from ${lastMessage.sender}: ${lastMessage.content}`);
}, [messages]);

This effect sends a browser notification every time the messages list is updated with a new message.

Techniques for ensuring smooth and lag-free user interactions

To ensure a fluid experience:

  1. Lazy-load heavy components:
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function Chat() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />

  1. Use Next.js's React Server Components to split logic:

Remember from the earlier documentation, Server Components can be used for non-interactive parts, while Client Components can handle the interactive parts, reducing the amount of JavaScript sent to the client.

For instance, in our chat, the message history can be a Server Component, while the input field and send button, which require client-side interactivity, can be Client Components.

7. Testing the Real-time Capabilities

With the core components of our real-time application in place, it's essential to ensure that they function as expected, and are performant, scalable, and robust. This section sheds light on various testing approaches tailored for real-time systems like our my-real-time-app.

Tools and strategies for testing real-time functionalities

  1. End-to-End Testing with Cypress

For real-time applications, end-to-end tests are crucial. Let's set up an example with Cypress:

// cypress/integration/chat.spec.js

describe('Chat functionality', () => {
  it('should send and receive messages in real-time', () => {
    cy.get('[data-cy=messageInput]').type('Hello, World!');
    cy.contains('Hello, World!').should('exist');

  1. Load Testing with Artillery

This will help in understanding how the system behaves under significant numbers of users or messages:

# artillery-config.yml

  target: ''
    - duration: 300
      arrivalRate: 20
  - flow:
      - emit:
          channel: 'chat'
            message: 'Hello, World!'
$ artillery run artillery-config.yml

Addressing potential bottlenecks and performance issues

  1. Profiling Server Performance

Node.js provides in-built tools for profiling, and the --inspect flag can be used with the Next.js development server to enable the Node.js inspector. By using Chrome's DevTools, one can get insights into performance bottlenecks.

  1. Client-side Performance Analysis

For the client-side, tools like the Performance tab in Chrome DevTools can help identify rendering bottlenecks. Especially with real-time updates, ensure that unnecessary renders aren't happening.

Ensuring scalability and robustness of the real-time application

  1. State Management with SWR or React Query

Real-time applications often involve keeping the client's state in sync with the server. Libraries like SWR or React Query help in making this easier by offering features like automatic re-fetching, caching, and real-time synchronization.

Example with SWR:

// my-real-time-app/app/chat-interface.js

import useSWR from 'swr';

function ChatInterface() {
  const { data: messages } = useSWR('/api/messages', fetcher);

  // ... rest of the component

  1. Horizontal Scaling

For backend scalability, especially with WebSockets, consider using a solution like Redis to manage the state across multiple instances of your server. This way, if one server instance receives a message, it can broadcast it to clients connected to other server instances.

  1. Database Optimization

Ensure that your database queries, especially those that run frequently in real-time applications, are optimized. Index essential columns, and consider using database caching solutions for frequently accessed data.

Notes: Testing real-time applications requires a combination of standard software testing techniques and some tailored specifically for the challenges and characteristics of real-time systems. Ensuring a rigorous testing regime for my-real-time-app, we can guarantee a smooth and responsive user experience, irrespective of the scale of user traffic or data flow.

10. Enhancements and Optimizations

With the foundational architecture of our real-time application firmly in place, our attention now turns to refining its features and performance. Here are some strategies to enhance the user experience and optimize our my-real-time-app:

Tips for enhancing the user experience

  1. Implementing Read Receipts

Provide visual feedback to users when their messages have been read by the recipient. This enhances the interactive nature of real-time chats.

// my-real-time-app/app/components/Message.js

function Message({ content, status }) {
  return (
      {status === 'read' && <span>✓ Read</span>}

  1. Displaying Online Status

Show an indicator next to a user's name or avatar when they are online.

// my-real-time-app/app/components/UserStatus.js

function UserStatus({ isOnline }) {
  return (
      {isOnline ? <span className="online-indicator"></span> : <span className="offline-indicator"></span>}

Optimizing server actions for reduced latency

  1. Server-Side Batching

Batch server-side updates where feasible to reduce the number of messages sent to the client.

  1. Compress WebSocket Messages

For applications with high-frequency updates, consider compressing WebSocket messages to reduce the data transferred and increase speed.

// Example: Setting up compression with a WebSocket server
const WebSocket = require('ws');
const wss = new WebSocket.Server({
  perMessageDeflate: {
    zlibDeflateOptions: {
      // Add compression options here

  1. Debounce Frequent Updates

If you're noticing rapid consecutive updates from clients, consider debouncing these to consolidate them into fewer, more meaningful updates.

Ensuring data integrity and fault tolerance

  1. Event Sourcing

For critical sections of your app where data integrity is paramount, consider adopting an event-sourcing pattern. This ensures every change to the application state is captured as an event, allowing for reliable recovery and replay of events.

  1. Implement Retry Logic

Ensure that if a message fails to send or an update doesn't go through due to network issues, there's a retry mechanism in place.

// Example: Simple retry logic with fetch
let retries = 3;

function fetchData(url) {
    .then(response => response.json())
    .catch(error => {
      if (retries > 0) {
      } else {
        console.error('Failed to fetch data after 3 retries');

  1. Backup and Recovery Plans

Regularly back up data and ensure you have a clear plan and processes to recover data in case of failures. Use database replication or distributed databases like Cassandra for fault tolerance.

Notes: The continued success of my-real-time-app hinges not just on its core functionalities but also on the subtle enhancements and constant optimizations that ensure a frictionless user experience. By incorporating the strategies listed above, we're poised to offer our users a superior chat experience that's reliable and delightful.

11. Conclusion and Future Prospects

Recap of the journey in building the real-time application

Our journey with my-real-time-app took us from the initial setup with Next.js 13.4, through backend building with server actions, designing a seamless frontend experience, and ensuring the real-time capabilities were tested and optimized. We delved deep into the nuances of server and client components, ensuring an effective balance between server-side rendering and client-side interactivity.

The impact and importance of Next.js 13.4's server actions in the project

The introduction of server actions in Next.js 13.4 revolutionized our approach to real-time applications. It allowed us to build a highly interactive chat application that leverages the strengths of both server and client rendering. This not only optimized performance but also facilitated seamless user interactions without compromising on security or efficiency.

Future enhancements and features that can be added to the application

While my-real-time-app has come a long way, the potential for future enhancements remains vast:

  1. Video Chat Integration: Introduce real-time video chat capabilities.
  2. Group Chats: Allow users to create, join, or leave group chats.
  3. End-to-End Encryption: Boost security by encrypting messages so that only the sender and recipient can decipher them.
  4. Customizable User Profiles: Give users the option to personalize their profile with avatars, status messages, and themes.
  5. Chatbots: Implement AI-driven chatbots for automated responses.

12. Resources and Further Reading

As you embark on your journey with real-time applications and dive deeper into the functionalities and intricacies of Next.js, here's a curated list of resources that can guide, inspire, and further educate you:

Official Documentation

  • Next.js Official Documentation: A comprehensive guide to everything that's new and improved in this version. Read here.
  • Server Actions in Next.js: A deep dive into the workings, best practices, and potentials of server actions, straight from the source. Read more.
  • The App Router: Understand the App Router's capabilities, especially concerning React Server Components. Explore here.
  • React Server Components: A primer on how to best utilize server components for optimized performance and flexibility. Learn here.

The end

First off, a massive thank you for journeying with me through this intricate maze of the Next.js world. If you've made it this far, congrats! If you skimmed through some parts, I don't blame you – there were times when I wanted to skip writing them!

Building real-time applications is, in many ways, a roller coaster of emotions. Some days I feel like a coding genius, while on others, I questioned every life choice that led me to become a developer.

Ever had those moments where you spend hours debugging an issue, only to realize you missed a semicolon? Or when you accidentally delete an essential part of your code and wish life had a Ctrl + Z? Oh, the joys of programming!

But here's the thing: amidst all the facepalms and occasional hair-pulling, there's an indescribable magic in seeing your creation come to life, in real-time. It’s that tiny spark of joy when your code runs without errors, the satisfaction when users love your app, and the pride in knowing you built something from scratch.

To every budding developer reading this: setbacks, frustrations, and 'why is this not working!?' moments are part and parcel of our journey. They aren’t signs that you're failing, but rather, stepping stones to becoming better.

So the next time your code refuses to cooperate, take a deep breath, grab some coffee (or tea, I don’t judge, I'm a matecocido fan myself), and remember you're not alone in this.

Keep pushing boundaries, keep learning, and remember that every line of code, whether it works or breaks, adds a chapter to your developer story.

And if you ever need a chuckle or a shoulder to cry on (virtually, of course), know that I've been there, done that, and have gotten frustrated enough to consider throwing my laptop out the window!

Here's to more coding adventures and fewer semicolon-induced bugs!

Cheers, and happy coding!