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.
- Find API key for Mailchimp
- @mailchimp/mailchimp_marketing
- @types/mailchimp__mailchimp_marketing
- node:crypto module
- Add Members to List
- List Members
- Add or Update List Member
- Route handling with Next.js
- Use client directive for Next.js
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!