DEPT® Engineering BlogTypeScript

How to integrate Mailchimp with Next JS and TypeScript

1. Introduction & Overview

In this tutorial, we will be going through how to integrate the email marketing platform Mailchimp with Next.js and TypeScript. When integrating it into an application recently, I ran across numerous issues and thought this tutorial might be useful to others. A GitHub repository is referenced at the end of this tutorial. 

Prerequisites

For your reference, below are the versions we are using in this application. We will be using the app router setup for Next.js.

Versions

  • Next: 15.1.4
  • React: 19.0.0
  • TypeScript: 5.6.2
  • Node: 22.12.0
  • NPM: 10.5.2

To integrate Mailchimp, you will need to sign up for an account. Install the npm packages @mailchimp/mailchimp_marketing and @types/mailchimp__mailchimp_marketing. For this tutorial, we are using @mailchimp/mailchimp_marketing version 3.0.80 and @types/mailchimp__mailchimp_marketing version 3.0.21. 

npm install @mailchimp/mailchimp_marketing @types/mailchimp__mailchimp_marketing

Note, the node:crypto module is a built-in module included with Node.  It doesn’t require installing, but it needs to be imported into the API endpoint to use the createHash function. 

Environment variables

Next you’ll need to set up MAILCHIMP_API_KEY, MAILCHIMP_API_SERVER, and MAILCHIMP_AUDIENCE_ID environment variables. Add these variables to your .env and then include them in your Next config file. 

1. MAILCHIMP_API_KEY: Mailchimp API key

This resource shows how to find an API key in your Mailchimp account. 

2. MAILCHIMP_API_SERVER: Mailchimp server value

To find the server value for your account, login to Mailchimp. After authentication, the browser URL will show the server value appended before “admin.mailchimp.com.” For example, if the URL was https://us19.admin.mailchimp.com/ the “us19” portion is the server prefix. 

3. MAILCHIMP_AUDIENCE_ID: Mailchimp audience ID

To find the Mailchimp audience ID, go to the Audience section in your account and then go to All contacts, and then to Settings. In the settings, there is an Audience ID field, which you can copy. 

2. Endpoint instructions

Since we are using the Next.js app router, we used this path for the endpoint: /src/app/api/mc/subscribeUser. If you are not using the app router, your setup will look slightly different. At the end of this tutorial, I will provide a GitHub repo that you could spin up to test out the setup with your account. 

For route handling, we are using the helpers NextRequest and NextResponse which are imported from next/server. There are other ways to accomplish route handling, check this resource for more information. 

In this file, we will also be importing Mailchimp, which we installed in the initial steps. We will also be importing createHash from the crypto module (node:crypto), which is included with Node. 

import { NextRequest, NextResponse } from 'next/server';
import mailchimp from '@mailchimp/mailchimp_marketing';
import { createHash } from "node:crypto";

Imports

From here we are ready to set the configuration for Mailchimp. The API key and server environment variables will be passed in as the apiKey and server parameters, as shown below. 

mailchimp.setConfig({
  apiKey: process.env.MAILCHIMP_API_KEY,
  server: process.env.MAILCHIMP_API_SERVER,
});

Set config function for Mailchimp module

Next, we will be setting up a POST request function. We are only requiring an email to create a list member, but first name and last name will be included if provided. We get the form values passed in using the json method (request.json()) and immediately fail if the email is not provided, since this will be required to create a member. We will be sending a failure response using the NextResponse route handler helper. 

We will also check to make sure the audience ID environment variable is available since this will also be required for the request and send a failure response if it is not available. 

export async function POST (request: NextRequest) {
  const body = await request.json();
  const { email, firstName, lastName } = body;
  if (!email) {
	return NextResponse.json({ error: 'Email is required.' }, { status: 400 });
  }
  const audienceId = process.env.MAILCHIMP_AUDIENCE_ID;
  if (!audienceId) {
	return NextResponse.json({ error: 'Audience required.' }, { status: 400 });
  }
  try {
	
   } catch (error: any) {
	
  }
};

Base setup for API endpoint

In this implementation, we check if the member exists in Mailchimp for that list so that we can communicate to the user that the failure was due to an account already being created. First, we need to create an MD5 hash of the email using the createHash function provided by the crypto Node module. Then, using the mailchimp.lists.getListMember method, we pass in the audience ID and the email hash.

If there was a member found, we can check if the status was subscribed. The resource for listing member info is available here and shows the other values for the status include: "subscribed", "unsubscribed", "cleaned", "pending", "transactional", or "archived". You can choose to handle each of those situations, but here, we are only handling already subscribed members. 

From there, we catch the error response so that it doesn’t fail if the user doesn’t have an account already. 

NOTE: Mailchimp has a method for adding or updating a list member, which is listed in the resources at the end if you’d prefer to use that method. 

const emailHash = createHash('md5').update(email).digest('hex');
const isEmailExisting = await mailchimp.lists.getListMember(audienceId, emailHash)
  .then((r) => {
    	const isSubscribed = r?.status === 'subscribed';
    	return isSubscribed;
  })
  .catch(() => false);
if (isEmailExisting) {
  	return NextResponse.json({ error: 'Email already subscribed.' }, { status: 400 });
}

Try catch statement with conditional for if list member exists

Now we can create the list member. The resource for adding a list member is available here. We will be using the mailchimp.lists.addListMember method and will pass in member data as well as the status of “subscribed.” From there we return the data if the request was successful.

const data = await mailchimp.lists.addListMember(audienceId, {
  email_address: email,
  status: 'subscribed',
  merge_fields: {
  	FNAME: firstName ?? "",
  	LNAME: lastName ?? "",
  },
});
return NextResponse.json({ data });

Add list member function

Finally, we will handle the error using the NextResponse json helper function (more information here). Add the following to the catch statement in the case of an unexpected error. 

let errorMessage = "";
if (error instanceof Error) {
  	errorMessage = error?.message;
} else {
  	errorMessage = errorMessage ?? error?.toString();
}
console.error(errorMessage);
return NextResponse.json(
  	{ error: "Something went wrong." },
  	{ status: 500 }
);

Error handling for API endpoint

Full snippet for API endpoint:

import { NextRequest, NextResponse } from "next/server";
import mailchimp from "@mailchimp/mailchimp_marketing";
import { createHash } from "node:crypto";

mailchimp.setConfig({
  apiKey: process.env.MAILCHIMP_API_KEY,
  server: process.env.MAILCHIMP_API_SERVER,
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { firstName, lastName, email } = body;
  if (!email) {
	return NextResponse.json({ error: "Email is required." }, { status: 400 });
  }
  const audienceId = process.env.MAILCHIMP_AUDIENCE_ID;
  if (!audienceId) {
	return NextResponse.json({ error: "Audience required." }, { status: 400 });
  }
  try {
	// Check if the email exists:
	const emailHash = createHash("md5").update(email).digest("hex");
	const isEmailExisting = await mailchimp.lists
    	.getListMember(audienceId, emailHash)
      	.then((r) => {
          	const isSubscribed = r?.status === "subscribed";
          	return isSubscribed;
    	})
    	.catch(() => false);
	if (isEmailExisting) {
    	return NextResponse.json(
        	{ error: "Email already subscribed." },
        	{ status: 400 }
    	);
	}
	// If the email doesn't exist, subscribe:
	const data = await mailchimp.lists.addListMember(audienceId, {
    	email_address: email,
    	status: "subscribed",
    	merge_fields: {
        	FNAME: firstName ?? "",
        	LNAME: lastName ?? "",
    	},
	});
	return NextResponse.json({ data });
  } catch (error: unknown) {
	let errorMessage = "";
	if (error instanceof Error) {
  		errorMessage = error?.message;
	} else {
  		errorMessage = errorMessage ?? error?.toString();
	}
	console.error(errorMessage);
	return NextResponse.json(
    	{ error: "Something went wrong." },
    	{ status: 500 }
	);
  }
}

Full snippet for API endpoint

3. UI instructions

We can now set up the form component, which will display a form that results in a list of members being created in Mailchimp. Create a React component that renders a basic form that has a submit form and input fields for email, first name, and last name. 

The file needs to start with the “use client” directive, which designates a component to be rendered on the client side. This should be used when creating interactive user interfaces (UI) that require client-side JavaScript capabilities; see resource here for more information. 

The CSS module file we’re importing (embeddedForm.module.css) has style specific to the site we were working on, so I’ll be glossing over that. 

  "use client";
import css from "./embeddedForm.module.css";

export function EmbeddedForm() {
  return (
	<form onSubmit={subscribeUser} className={css.form}>
    	<h2 className={css.header}>Subscribe to our newsletter!</h2>
    	<div className={css.inputWrapper}>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>First name</span>
            	<input name="firstName" className={css.inputField} />
        	</label>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>Last name</span>
            	<input name="lastName" className={css.inputField} />
        	</label>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>Email</span>
            	<input name="email" type="email" className={css.inputField} />
        	</label>
    	</div>
    	<button type="submit" value="" name="subscribe" className={css.button}>
        	Submit
    	</button>
	</form>
  );
}

export default EmbeddedForm;

Base EmbeddedForm component

Now, we will set up the function that uses our new subscribeUser endpoint which creates a list member in Mailchimp. Since we are rendering a form element, the event parameter type will be FormEvent with HTMLFormElement passed in. We will need to import FormEvent from react to accomplish this. 

We will also need to import useState from React. From there, we can set up the isLoading state that we will use to display a loader component, as well as a message state that will show the user the result after they submit the form. 

Within the subscribeUser function, we will use the preventDefault method that cancels the event if it is a default action, such as if someone hits Submit without filling out any information. Then, we will set the isLoading state to true so that the loading spinner will display while attempting the request. 

The form contains a first name, last name, and email, which will be sent to the Mailchimp endpoint for creating a list member. To retrieve the form data, create a new FormData object using the FormData constructor and pass in the currentTarget property from the event which will have the first name, last name and email values provided in the form. 

From there, we use the fetch function to hit the endpoint we created (/api/mc/subscribeUser) and provide the form data included in the form. We will also need to set the message state to share the status with the user, which will be either a success or error message,  and set isLoading to false so that the message displays to the user instead of the loader. 

const subscribeUser = async (e: FormEvent<HTMLFormElement>) => {
	e.preventDefault();
	setIsLoading(true);
	const formData = new FormData(e.currentTarget);
	const firstName = formData.get("firstName");
	const lastName = formData.get("lastName");
	const email = formData.get("email");
	const response = await fetch("/api/mc/subscribeUser", {
    	body: JSON.stringify({
        	email,
        	firstName,
        	lastName,
    	}),
    	headers: {
      		"Content-Type": "application/json",
    	},
    	method: "POST",
	});
	const json = await response.json();
	const { data, error } = json;
	if (error) {
    	setIsLoading(false);
    	setMessage(error);
    	return;
	}
	setMessage("You have successfully subscribed.");
	setIsLoading(false);
	return data;
};

subscribeUser function

After creating the function, add a conditional that displays the CircularLoader component when isLoading is set to true. The CircularLoader is featured in the Github repo, but any loader component will do. 

We also need to add a conditional that displays the message if there is one set, which will happen if the request was successful or not. 

Full snippet for UI component:

"use client";
import { FormEvent, useState } from "react";
import CircularLoader from "@/components/loader/circular-loader";
import css from "./embeddedForm.module.css";

export function EmbeddedForm() {
  const [isLoading, setIsLoading] = useState(false);
  const [message, setMessage] = useState("");

  const subscribeUser = async (e: FormEvent<HTMLFormElement>) => {
	e.preventDefault();
	setIsLoading(true);
	const formData = new FormData(e.currentTarget);
	const firstName = formData.get("firstName");
	const lastName = formData.get("lastName");
	const email = formData.get("email");
	const response = await fetch("/api/mc/subscribeUser", {
    	body: JSON.stringify({
        	email,
        	firstName,
        	lastName,
    	}),
    	headers: {
      		"Content-Type": "application/json",
    	},
    	method: "POST",
	});
	const json = await response.json();
	const { data, error } = json;
	if (error) {
    	setIsLoading(false);
    	setMessage(error);
    	return;
	}
	setMessage("You have successfully subscribed.");
	setIsLoading(false);
	return data;
  };

  if (message) {
	return <p className={css.errorMessage}>{message}</p>;
  }

  if (isLoading) {
	return <CircularLoader />;
  }

  return (
	<form onSubmit={subscribeUser} className={css.form}>
    	<h2 className={css.header}>Subscribe to our newsletter!</h2>
    	<div className={css.inputWrapper}>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>First name</span>
            	<input name="firstName" className={css.inputField} />
        	</label>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>Last name</span>
            	<input name="lastName" className={css.inputField} />
        	</label>
        	<label className={css.inputAndLabel}>
            	<span className={css.inputFieldLabel}>Email</span>
            	<input name="email" type="email" className={css.inputField} />
        	</label>
      	</div>
    	<button type="submit" value="" name="subscribe" className={css.button}>
      	Submit
    	</button>
	</form>
  );
}

export default EmbeddedForm;

Full snippet for UI component

4. Additional resources

To test this functionality from this tutorial, you can clone this Github repository and change the environment variables to reference your own Mailchimp account: https://github.com/dallashuggins/mailchimp-nextjs

Below are the resources listed throughout this tutorial for quick reference.

5. Conclusion

We hope you found this tutorial useful for integrating Mailchimp into your Next.js application. 

We welcome any feedback or questions in the comments here or in the linked Github repo. Thanks for reading!