DEV Community

Cover image for Implementing stateless session for Next.js using Server Actions
Wang Sijie for Logto

Posted on • Originally published at blog.logto.io

Implementing stateless session for Next.js using Server Actions

Introduction

Following the much-celebrated release of the App Router released, Next.js introduced another feature, Server Actions. This new innovation facilitates server-side data manipulations, reduces reliance on client-side JavaScript, and progressively enhances forms–all while using JavaScript and React to create robust web applications without the need for traditional REST APIs.

In this article, we tap into the wealth of advantages offered by this innovation and see it in action as we implement a cookie-based stateless session for Next.js. This piece serves as a step-by-step guide that will walk you through every phase of crafting a demo project using the App Router.

Our practical demonstration can be readily deployed to Vercel using Edge Runtime. And you can download the full source code from GitHub.

Steering away from REST APIs

Traditionally, if we want to create a Next.js web app that queries database in the backend, we may create some REST APIs to validate auth state and query database, then the front-end React app will call these APIs. But if there is no need to open API to the public and this react app is the only client, it seems redundant to use REST APIs for they will only be called by ourselves.

With Server Actions, the React component can now run server side code. Rather than needing to manually create an API endpoint, Server Actions automatically create an endpoint for Next.js to use behind the scenes. When calling a Server Action, Next.js sends a POST request to the page you're on with metadata for which action to run.

The need for stateless session

As a preferred framework for creating stateless applications, Next.js often means serverless, in which we can not use the memory to store session data. Traditional sessions necessitate the use of an external storage service, such as Redis or a database.

However, when the session data remains small enough, we have an alternative path: engineer a stateless session using secure encryption methods and cookies stored on the client-side. This method bypasses the need for external storage and keeps the session data decentralized, offering non-trivial benefits regarding server load and overall application performance.

So our target is a lightweight, efficient stateless session that capitalizes on the client-side storage capability while ensuring security through well-executed encryption.

Basic session implementation

First up, we need to initiate a new project:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

For more details on the installation, refer to the official guide.

Crafting a session library

To make understanding Server Actions easier, we'll create a simplified version of the session first. In this version, we'll use JSON to store session data in cookies.

Create a file called session/index.ts and include the following code:

'use server';

import { cookies } from 'next/headers';

export type Session = {
  username: string;
};

export const getSession = async (): Promise<Session | null> => {
  const cookieStore = cookies();
  const session = cookieStore.get('session');

  if (session?.value) {
    return JSON.parse(session.value) as Session;
  }

  return null;
};

export const setSession = async (session: Session) => {
  const cookieStore = cookies();
  cookieStore.set('session', JSON.stringify(session));
};

export const removeSession = async () => {
  const cookieStore = cookies();
  cookieStore.delete('session');
};
Enter fullscreen mode Exit fullscreen mode

The first line "use server" marks this file’s functions as Server Actions.

Since we cannot access request and response directly, we use next/headers to read and write cookies. This is only available in Server Actions.

Incoming: two more Server Actions

With the session library in place, it's time to implement a sign in and sign out feature to showcase the session's usability.

Incorporate the following code into user/index.ts:

'use server';

import { removeSession, setSession } from '@/session';

export const signIn = async (username: string) => {
  await setSession({ username });
};

export const signOut = async () => {
  await removeSession();
};
Enter fullscreen mode Exit fullscreen mode

Here, we are using a 'pretend' sign in process that merely requires a username.

Building the page

In App Router, the page is usually an asynchronous component, and Server Actions cannot be directly invoked from such a component. We need to create components using "use client":

components/sign-in.tsx

'use client';

import { signIn } from '@/user';
import { useState } from 'react';

const SignIn = () => {
  const [username, setUsername] = useState('');

  return (
    <div>
      <input
        type="text"
        value={username}
        placeholder="username"
        onChange={(event) => {
          setUsername(event.target.value);
        }}
      />
      <button
        disabled={!username}
        onClick={() => {
          signIn(username);
        }}
      >
        Sign In
      </button>
    </div>
  );
};

export default SignIn;
Enter fullscreen mode Exit fullscreen mode

components/sign-out.tsx

'use client';

import { signOut } from '@/user';

const SignOut = () => {
  return (
    <button
      onClick={() => {
        signOut();
      }}
    >
      Sign Out
    </button>
  );
};

export default SignOut;
Enter fullscreen mode Exit fullscreen mode

Finally, let's construct our app/page.tsx

import { getSession } from '@/session';
import styles from './page.module.css';
import SignIn from '../components/sign-in';
import SignOut from '@/components/sign-out';

export default async function Home() {
  const session = await getSession();

  return (
    <main className={styles.main}>
      {session ? (
        <div>
          <div>You have signed in as {session.username}</div>
          <div>
            <SignOut />
          </div>
        </div>
      ) : (
        <SignIn />
      )}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Implementing encryption

The job of Server Actions is done. Now, the final part involves the encryption implementation that can be achieved through crypto.

// session/encrypt.ts
import { createCipheriv, createDecipheriv } from 'crypto';

// Replace with your own key and iv
// You can generate them with crypto.randomBytes(32) and crypto.randomBytes(16)
const key = Buffer.from('17204a84b538359abe8ba74807efa12a068c20a7c7f224b35198acf832cea57b', 'hex');
const iv = Buffer.from('da1cdcd9fe4199c835bd5f1d56446aff', 'hex');
const algorithm = 'aes-256-cbc';

export const encrypt = (text: string) => {
  const cipher = createCipheriv(algorithm, key, iv);
  const encrypted = cipher.update(text, 'utf8', 'base64');
  return `${encrypted}${cipher.final('base64')}`;
};

export const decrypt = (encrypted: string) => {
  const decipher = createDecipheriv(algorithm, key, iv);
  const decrypted = decipher.update(encrypted, 'base64', 'utf8');
  return `${decrypted}${decipher.final('utf8')}`;
};
Enter fullscreen mode Exit fullscreen mode

Next, modify the session library to implement the following:

'use server';

import { cookies } from 'next/headers';
import { decrypt, encrypt } from './encrypt';

export type Session = {
  username: string;
};

export const getSession = async (): Promise<Session | null> => {
  const cookieStore = cookies();
  const session = cookieStore.get('session');

  if (session?.value) {
    try {
      const decrypted = decrypt(session.value);
      return JSON.parse(decrypted) as Session;
    } catch {
      // Ignore invalid session
    }
  }

  return null;
};

export const setSession = async (session: Session) => {
  const cookieStore = cookies();
  const encrypted = encrypt(JSON.stringify(session));
  cookieStore.set('session', encrypted);
};

export const removeSession = async () => {
  const cookieStore = cookies();
  cookieStore.delete('session');
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Congratulations! You've successfully implemented a stateless session for Next.js. Here is an online preview on Vercel, and you can download the full source code here. We hope this guide aids your understanding of the brand new Server Actions.

Next, we'll be exploring how to use Server Actions to integrate Logto for Next.js. Stay tuned!

Top comments (0)