SOP: connect a Nuxt form to MailerLite without replacing SendGrid
Connect one Nuxt form to MailerLite while preserving SendGrid delivery with server-only secrets, explicit field mapping and safe failure handling.
A Nuxt MailerLite integration should not replace a working SendGrid form flow. The safe pattern is to keep SendGrid responsible for the form’s transactional delivery, then let the Nuxt server selectively create or update a MailerLite subscriber for forms that have been explicitly approved for automation.
That separation protects API keys, prevents unrelated contact forms from entering a marketing workflow, and handles partial success honestly. This SOP applies to Nuxt applications on Vercel, including maintained Nuxt 3 projects and new Nuxt 4 projects.
The short answer
Selected Vue form
→ POST /api/forms/submit
→ validate and normalize once
→ SendGrid delivery or existing fallback
→ optional MailerLite subscriber upsert + group assignment
→ response based on the form's primary purpose
The browser never receives the MailerLite token. Only the Nuxt server calls MailerLite, and only a registered formType can do so. The same principle applies to any form integration with private credentials, including the server-side contact flow in this SendGrid and Orbitype SOP.
SendGrid and MailerLite solve different problems
Both services send email, but they should not be modeled as one pipeline.
| Service | Responsibility in this workflow |
|---|---|
| SendGrid | Deliver the lead notification and other immediate transactional messages. |
| MailerLite | Maintain subscriber data, group membership and marketing automation. |
| Nuxt server route | Validate the form, coordinate both adapters and apply the failure policy. |
MailerLite can start an automation when a subscriber joins a group. That makes group membership a useful trigger without coupling campaign delivery to the application. Its automation trigger documentation also explains that re-entry depends on the workflow settings and that someone already in a group must leave it before joining again.
Use the API for an outbound form submission
The website needs to send data to MailerLite, so the Nuxt server makes an outbound REST request. A MailerLite webhook is the reverse direction: it lets MailerLite notify your application about something that happened in MailerLite. It does not create or update a subscriber from a website form.
The relevant API operation is:
POST https://connect.mailerlite.com/api/subscribers
Authorization: Bearer <server-only-token>
Content-Type: application/json
Accept: application/json
MailerLite documents this endpoint as a create-or-update operation. Existing subscribers are updated without removing omitted fields or groups; supplied group IDs add membership. Consult the Subscribers API reference for the current request contract and response behavior.
1. Decide what success means before writing code
The endpoint should not use the same failure response for every form. Define the primary operation first.
| Policy | Primary operation | MailerLite failure | SendGrid failure |
|---|---|---|---|
contact | Deliver a request | Log the secondary failure; keep the request successful. | Return an error. |
newsletter | Create a subscription | Do not report subscription success. | Usually not applicable. |
mixed | Deliver a lead and start a follow-up flow | Preserve lead success; record the sync failure. | Return an error. |
A lead-magnet form often uses mixed: the visitor’s request has succeeded once transactional delivery succeeds, while MailerLite enables the follow-up workflow. A pure newsletter subscription has a different promise and should make a MailerLite failure visible instead of claiming success.
2. Keep the token in private Nuxt runtime configuration
Never expose the token through runtimeConfig.public, a NUXT_PUBLIC_* variable, a Vue component, CMS content, browser request or committed fixture.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
mailerliteApiKey: "",
},
})
# .env.example — server-only placeholder
NUXT_MAILERLITE_API_KEY="your-mailerlite-api-token"
Nuxt keeps keys declared directly in runtimeConfig server-only, while keys under runtimeConfig.public are available to client-side code. See Nuxt’s runtime configuration guide before choosing names and environment overrides.
For Vercel, configure the value in the intended Development, Preview and Production environments. A variable change applies to a new deployment rather than an existing one, so redeploy after editing it. Vercel’s environment variable guide documents that deployment behavior.
3. Use a per-form registry as an allowlist
The API key is a secret. A group ID is configuration, but it should still be explicit: it ties a form to an automation and defines the fields allowed to leave the application.
// server/utils/mailerliteFormConfig.ts
export type MailerLiteFailurePolicy = "contact" | "newsletter" | "mixed"
export type MailerLiteFormConfig = {
groupId: string
groupName?: string
emailField: string
fieldMapping: Record<string, string>
consentField?: string
failurePolicy: MailerLiteFailurePolicy
}
const MAILERLITE_FORMS = {
lead_magnet: {
groupId: "YOUR_MAILERLITE_GROUP_ID",
groupName: "Lead Magnet — PDF Guide",
emailField: "email",
fieldMapping: {
firstName: "name",
lastName: "last_name",
phone: "phone",
},
failurePolicy: "mixed",
},
} satisfies Record<string, MailerLiteFormConfig>
export function getMailerliteFormConfig(formType: string) {
return MAILERLITE_FORMS[formType as keyof typeof MAILERLITE_FORMS]
}
This registry gives four useful guarantees: unregistered forms make no MailerLite call; every registered form has its own group and policy; fieldMapping is a whitelist; and any consent rule is visible beside the integration. Create custom fields in MailerLite first, then use their internal field names in the mapping.
4. Keep the MailerLite client small and server-only
The HTTP client should not know form names, Vue components or SendGrid. It should return a safe result and let the endpoint decide the user-facing outcome.
const MAILERLITE_SUBSCRIBERS_URL =
"https://connect.mailerlite.com/api/subscribers"
export async function upsertSubscriberToGroups({
apiKey,
email,
fields,
groupIds,
timeoutMs = 10_000,
}: {
apiKey: string
email: string
fields?: Record<string, string>
groupIds: string[]
timeoutMs?: number
}) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(MAILERLITE_SUBSCRIBERS_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ email, fields, groups: groupIds }),
signal: controller.signal,
})
return response.ok
? { ok: true as const, status: response.status }
: { ok: false as const, status: response.status }
} catch {
return { ok: false as const }
} finally {
clearTimeout(timeout)
}
}
Do not force status: "active" or resubscribe: true by default. Reactivating a previously unsubscribed, bounced or junk contact is a consent and product decision, not a technical convenience. Never log tokens, email addresses, request bodies or full provider responses.
5. Coordinate both adapters in the endpoint
Validate the submission once, perform the primary operation, then evaluate the optional integration.
const fields = validateAndNormalizeFormSubmission(body)
const delivery = await sendTransactionalForm({
formType: body.formType,
fields,
})
if (!delivery.ok) {
throw createError({ statusCode: 502, statusMessage: "Please try again." })
}
const config = getMailerliteFormConfig(body.formType)
const email = config ? fields[config.emailField] : undefined
if (config && email && hasMailerliteConsent(fields, config)) {
const result = await upsertSubscriberToGroups({
apiKey: useRuntimeConfig(event).mailerliteApiKey,
email,
fields: buildMailerliteFields(fields, config.fieldMapping),
groupIds: [config.groupId],
})
if (!result.ok && config.failurePolicy === "newsletter") {
throw createError({ statusCode: 502, statusMessage: "Subscription failed." })
}
}
return { ok: true }
For a mixed form, this sequence produces controlled partial success: SendGrid has delivered the lead, the application records a safe MailerLite failure if one occurs, and the visitor is not encouraged to submit the same lead again. If recovery is important, use a queue or durable retry process rather than repeated synchronous requests in the form handler.
6. Treat consent and data mapping as product decisions
An automation may be part of delivering a requested resource, while broader marketing may require separate consent. The integration cannot infer that distinction.
When a checkbox is required, make it explicit in the registry:
lead_magnet: {
// other configuration
consentField: "marketingConsent",
failurePolicy: "mixed",
}
Normalize the value and check it server-side. This is an implementation pattern, not legal advice: the form language, automation content, markets and privacy policy still need appropriate review.
7. Test the behavior without touching production data
Unit tests should mock fetch; an automated test must not create a real subscriber. Cover at least these cases:
- an unregistered
formTypedoes not call MailerLite; - only allowlisted fields appear in the payload;
- consent accepts and rejects normalized values as expected;
- the Bearer header, URL and group ID are correct;
200and201are treated as success;- missing configuration, timeouts,
422and429are controlled failures; - a MailerLite failure preserves a successful
mixedform response; - a SendGrid failure still returns the primary delivery error.
Run a real smoke test only as an explicit, manual operation with an authorized address. Check the group afterward and remove test data when appropriate. Do not run that test in unit tests, pull requests or deployment hooks.
Implementation checklist
- Only registered forms can call MailerLite.
- SendGrid remains the transactional adapter.
- The API token is private server configuration.
- Group IDs, field allowlists and policies live in the registry.
-
statusandresubscribeare omitted unless explicitly approved. - Consent behavior matches the form’s purpose.
- Unit tests mock external requests.
- Preview and production environment values are configured and redeployed.
- Logs exclude tokens and unnecessary personal data.
The reusable insight is not the fetch call. It is making each form’s purpose, permitted data, consent and failure policy explicit. With that structure, SendGrid keeps doing the transaction it already owns and MailerLite adds the selected subscriber workflow without becoming an accidental dependency for every form.