Skip to main content
Server Actions on NextJs to Call External APIs

Server Actions on NextJs to Call External APIs

Written on March 01, 2024 by Aldy Azarya.

15 min read

Next.js, a fantastic React framework, brings the magic of static site generation (SSG), server-side rendering (SSR), and incremental static regeneration (ISR). Known for its speed, scalability, and user-friendly approach, Next.js 14 introduces a cool addition called Server Actions.

Imagine having the power to run server-side code without the hassle of creating dedicated API routes. With Server Actions, you can do just that! Whether it's fetching data from external APIs, handling business logic, or updating your database, Server Actions make these tasks a breeze. It's like having a handy helper for all your server-side needs!

Understanding Next.js Server Actions

Server Actions joined the Next.js family in version 13, but it's in version 14 that they become a stable and integral part of the framework.

These nifty Server Actions serve various purposes and are particularly handy for:

  • Getting data from external APIs: Easily fetch data from external APIs without sacrificing performance or security.
  • Handling business logic: Let Server Actions take care of server-side execution for business logic tasks like validating user input or processing payments.
  • Database updates made easy: Update your database seamlessly without the need for creating additional API routes.

How Server Actions work

Next.js Server Actions are like helpful assistants that handle tasks on the server when users do something on your website. It's a bit technical, but here's the gist:

  1. Imagine a user does something or a specific condition is met on your site. That triggers a call to a Server Action, just like making a request.
  2. Next.js takes care of packaging up all the info (like form data or URL details) and sends it to the server.
  3. The server then understands and performs the Server Action function.
  4. Once the server finishes its job, it sends the results back to Next.js, which then passes it back to the user's screen.
  5. After the behind-the-scenes work is done, the user's side of things keeps going. It's like a teamwork dance between the user's actions and the server doing its thing!

The syntax for defining Server Actions

You can define Server Actions in two places:

  • In the server component that uses it.
// app/page.ts
export default function ServerComponent() {
  async function myAction() {
    'use server'
    // ...
  }
}
  • Or in a separate file for reusability.
// app/actions.ts
'use server'
 
export async function myAction() {
  // ...
}

How to invoke Server Actions

  • Using the action prop.

You can use the action prop to invoke a Server Action from any HTML element, such as a <button><input type="submit">, or <form>.

For example, the following code will invoke the likeThisArticle Server Action when the user clicks the "Add to Shopping Cart" button:

<button type="button" action={likeThisArticle}>Like this article</button>
  • Using the useFormState hook.

You can use the formAction prop to invoke a Server Action from a <form> element.

For example, the following code will invoke the addComment Server Action when the user submits the form:

'use client'

import { useFormState } from 'react';
import { addComment } from '@/actions/add-comment';

export default function ArticleComment({ initialState }) {
  const [state, formAction] = useFormState(addComment, initialState)

  return (
    <form onClick={formAction}>
      Add Comment
    </button>
  )
}
  • Using the startTransition hook.

You can use the startTransition hook to invoke a Server Action from any component in your Next.js application.

For example, the following code will invoke the addComment Server Action.

'use client'
    
    import { useTransition } from 'react';
    import { addComment } from '@/actions/add-comment';
    
    export default function ArticleComment() {
      const [isPending, startTransition] = useTransition()
    
        function onAddComment() {
            startTransition(() => {
                addComment('This article is nothing but great!');
        });
        }
    
      return (
        <button onClick={() => onAddComment()}>
          Add Comment
        </button>
      )
    }

The startTransition hook ensures that the state update batches with other state updates that are happening at the same time. Using startTransition can improve the performance of your application by reducing the number of re-renders that are required.

Which method should I use?

Choosing how to trigger a Server Action depends on what you're aiming to do. If you want to activate a Server Action from a <form> element, go with the formAction prop. If it's from a component, opt for the startTransition hook. Pick the method that suits your needs best!

Sending Data to an External API Using Server Actions

Now that we have a grasp of how Server Actions work, let's dive into practical examples, like using Server Actions to smoothly send data to a third-party API.

To kick things off, we'll set up a Server Action that activates when a form is submitted. This differs from other functions triggered by regular JavaScript calls.

Let's create a fresh Server Action named addComment. This nifty function will anticipate a single parameter, FormData type, just like this example:

// /services/actions/comment
'use server'

export async function addComment(formData) {
  const articleId = formData.get('articleId');
  const comment = formData.get('comment');
  const response = await fetch(`https://api.example.com/articles/${articleId}/comments`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ comment }),
  });
  const result = await response.json();
  return result;
}

That's all it takes to set up a Server Action. Now, let's shift our attention to triggering this function when a user submits a form.

We'll start by crafting a fresh component responsible for displaying the 'Add Comment' form.

import { addComment } from '@/services/actions/comment'
 
export default async function ArticleComment(props) {
  return (
    <form action={addComment}>
      <input type="text" name="articleId" value={props.articleId} />
      <input type="text" name="comment" />
            <button type="submit">Add Comment</button>
    </form>
  )
}

Great job! Your Server Action is all set up and ready to go, but when you test it, you might notice that it seems a bit unresponsive. In other words, there's no indication to the user that something is happening – no "loading" or "saving" activity displayed. Let's add that next to enhance the user experience!

Displaying the loading state

You can make use of the useFormStatus hook to show a friendly loading state when your form is in the process of being submitted. Since it's a hook, remember that it should be placed in a client-side component and used as a child within a form element utilizing a Server Action.

Now, let's spruce up our existing component, ArticleComment. Instead of the regular button, let's swap it out with a custom component we've crafted ourselves, and we'll call it AddCommentButton.

import { addComment } from '@/services/actions/comment'
import { AddCommentButton } from '@/components/AddCommentButton'
 
export default async function ArticleComment(props) {
  return (
    <form action={addComment}>
      <input type="text" name="articleId" value={props.articleId} />
      <input type="text" name="comment" />
            <AddCommentButton />
    </form>
  )
}

And, of course, you’ll have to create such a component:

'use client'
 
import { useFormStatus } from 'react-dom'
 
export function AddCommentButton() {
  const { pending } = useFormStatus();
 
  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

Now, once you submit the form, the updated status will be mirrored in the "pending" variable, and it will dynamically adjust the button component. Feel free to enhance this code to offer additional visual cues, making the user experience even more delightful and engaging.

Submitting Files to Server Actions

No need to stress about handling more than just JSON data or file uploads with Server Actions—it's got you covered!

To send texts, numbers, or upload files, it's as simple as your usual Server Actions setup. Whether you're using a form with <input type="file" /> or cool libraries like dropzone, there's nothing tricky on the client side.

On the server side, things might vary based on your needs. You could save the file, give it a makeover, or whatever suits your fancy. Retrieving the file follows the same easy process.

Check out this example of a Server Action effortlessly managing a file upload and sending it to an API using a stream:

// /services/actions/upload-file
'use server'

export async function uploadFile(formData) {
  const comment = formData.get('file');
  const arrayBuffer = await file.arrayBuffer();
  const buffer = new Uint8Array(arrayBuffer);
  await new Promise((resolve, reject) => {
    upload_stream({}, function (error, result) {
      if (error) {
        reject(error);
        return;
      }
      resolve(result);
    })
    .end(buffer);
  });
}

It is that simple!

Keep in mind that Server Actions come with a default size limit of 1 MB, but don't worry – you can always customize it based on your needs. For additional details, take a look at the official documentation on Server Action size limitations.

Best Practices for Using Server Actions

We've delved quite a bit into Server Actions, but it's such a vast topic that cramming it all into one article isn't practical. However, before we part ways, I'd love to share some friendly advice and handy tips for working with Server Actions and beyond. I'll also throw in some insights on tapping into protected resources using Auth0 and Next.js Server Actions.

  • Separate Components and Actions: While you can toss your Server Actions into a Server Component if it's a one-time thing, it's wise to follow the "Clean Code" mantra. Keeping components and actions separate makes your code more understandable, maintainable, reusable, and testable.
  • Don't Forget the Client Side: With the enchantment of Server Components, the line between server and client can blur. But, don't let the UI suffer. Shifting validations to the server might cut a few lines of code, but it could impact user experience. Keep that balance!
  • Cache Results: If your Server Action dishes out data that doesn't change often, consider caching the results. It's a nifty trick to skip unnecessary server trips, giving your application a performance boost.
  • Graceful Error Handling: Mishaps happen. If a Server Action hits a bump, handle that error with finesse. A meaningful error message goes a long way in keeping users in the loop.
  • Secure Your Server Actions: Security first! Treat your Server Actions like you would an API endpoint. They deserve protection from unauthorized snooping. Keep those codes under lock and key!

Calling a Protected API Endpoint

Shielding APIs is like giving them a secret handshake – it keeps sensitive data safe and only lets the right folks in. There are cool tricks to do this:

  • Magic Keys (API Keys): It's like a VIP pass. Only the cool, authorized friends get it. When they want to chat with the API, they just flash the pass (send the key), and the API checks it to make sure they're legit before opening the door.
  • Access Tokens: Think of them as golden tickets but for apps. After a successful login, the app gets this special ticket (access token). Now, whenever it wants to talk to the API, it just shows the ticket with its requests. And just like that, the API knows it's dealing with the right player. JSON Web Tokens (JWTs) are like the stylish version of these tickets.

API Keys to protect API endpoints

An API key is like a secret password for your app when talking to an API. It helps the API know your app and validate its requests. But here's the catch: while API keys are good for securing access, they don't know who's actually using them.

So, anyone with the API key can use the API, no questions asked. To fix this, you might want to check out OAuth 2.0. It's a way cooler way of handling things, and we've got a guide on "Why You Should Migrate to OAuth 2.0 from API Keys" that you should totally check out.

Remember, every project handles API keys differently. So, depending on how your favorite API likes its keys, you might need to tweak how you use Server Actions to talk to it. Let's spice up our addComment Server Action by passing it an API key for that extra security layer!

// /services/actions/comment
'use server'

export async function addComment(formData) {
    const articleId = formData.get('articleId');
    const comment = formData.get('comment');
  const response = await fetch(`https://api.example.com/articles/${articleId}/comments`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    'x-api-key': process.env.CMS_API_KEY // 👈 New Code
    },
    body: JSON.stringify({ comment }),
  });
  const result = await response.json();
  return result;
}

The server function stays the same, just add a new bit to the request headers. In the example, we use x-api-key, but it might be different for your API.

Since Server Actions run on the server, it's totally safe to grab API keys from environment variables. Just check if your environment provider is cool with it. For instance, if you deploy with Vercel, all your environmental variable values get encrypted automatically when at rest.

API keys are handy in lots of situations. But sometimes, your API needs to know who's doing the action. Like, right now, our API doesn't know who's making the comment. Sure, it could be added in the payload, but it's not safe if we can't confirm it's the actual user doing the thing.

Access Tokens to protect API endpoints

Access tokens play a crucial role in token-based authentication, allowing applications to connect with APIs. Once a user successfully goes through the authentication and authorization steps, they receive an access token. This token acts as a passcode when the application communicates with the API, signaling that the user has the green light to perform specific actions based on the granted permissions.

Let's simplify the process into two steps:

  • Authentication: Identifying a user and getting an access token.
  • Calling an endpoint: Using the received access token to interact with the API.

Handling access tokens involves a series of tasks – authentication, generation, verification – and it's a bit of a puzzle. Making a mistake in this process could jeopardize your system's data and security. That's why many app developers prefer relying on established authentication and authorization providers to handle these complexities securely.

Once your users are authenticated, you can rewrite your Server Action as follows:

// /services/actions/comment
'use server'
import { getAccessToken } from '@auth0/nextjs-auth0' // 👈 New Code

export async function addComment(formData) {
  const accessToken = await getAccessToken(); // 👈 New Code
  const articleId = formData.get('articleId');
  const comment = formData.get('comment');
  const response = await fetch(`https://api.example.com/articles/${articleId}/comments`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken.accessToken}`, // 👈 New Code
    },
    body: JSON.stringify({ comment }),
  });
  const result = await response.json();
  return result;
}

Conclusion

Server Actions in Next.js are an awesome new addition that empowers developers to achieve more with less code. This feature eliminates the need for extensive boilerplate typically required for writing API codes and their associated calling code.

In this guide, we've explored the basics of Server Actions, delving into the specifics of interacting with third-party public and protected API endpoints in a user-friendly manner.

Tweet this article