All posts

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.

Paper-cut routing system that directs one form submission to transactional email and an optional subscriber group

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.

ServiceResponsibility in this workflow
SendGridDeliver the lead notification and other immediate transactional messages.
MailerLiteMaintain subscriber data, group membership and marketing automation.
Nuxt server routeValidate 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.

PolicyPrimary operationMailerLite failureSendGrid failure
contactDeliver a requestLog the secondary failure; keep the request successful.Return an error.
newsletterCreate a subscriptionDo not report subscription success.Usually not applicable.
mixedDeliver a lead and start a follow-up flowPreserve 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.

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 formType does 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;
  • 200 and 201 are treated as success;
  • missing configuration, timeouts, 422 and 429 are controlled failures;
  • a MailerLite failure preserves a successful mixed form 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.
  • status and resubscribe are 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.

// stuck on something similar?

Let's debug it together

Book a call More posts