Send email from an Angular contact form

An Angular contact form that emails you every submission without a Node server or email API. A standalone component with reactive forms and signals.

Free for 100 submissions a month, no credit card. Updated

The usual advice for sending email from an Angular contact form is a Node server with Nodemailer, or EmailJS. The first means hosting a server and guarding SMTP credentials for one form. EmailJS sends from the browser, but you manage its templates and monthly quota and add your own spam checks.

The component below posts to a Nisuform endpoint instead. The endpoint filters spam, keeps every submission in an inbox and emails you, and your app stays a static build.

The component

contact-form.component.ts
import { Component, inject, signal } from '@angular/core'
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'

const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY'

@Component({
  selector: 'app-contact-form',
  imports: [ReactiveFormsModule],
  templateUrl: './contact-form.component.html',
})
export class ContactFormComponent {
  private readonly shownAt = Date.now()
  readonly status = signal<'idle' | 'sending' | 'sent' | 'error'>('idle')
  readonly error = signal('')
  readonly form = inject(FormBuilder).nonNullable.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    message: ['', Validators.required],
    _gotcha: [''],
  })

  async submit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched()
      return
    }
    this.status.set('sending')
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify({ ...this.form.getRawValue(), _elapsed: String(Date.now() - this.shownAt) }),
      })
      const result = await response.json()
      if (result.ok) {
        this.status.set('sent')
      } else {
        this.error.set(result.message)
        this.status.set('error')
      }
    } catch {
      this.error.set('Something went wrong. Please try again.')
      this.status.set('error')
    }
  }
}
contact-form.component.html
@if (status() === 'sent') {
  <p role="status">Thanks! We got your message.</p>
} @else {
  <form [formGroup]="form" (ngSubmit)="submit()">
    <label>
      Name
      <input formControlName="name" type="text" autocomplete="name" />
    </label>
    <label>
      Email
      <input formControlName="email" type="email" autocomplete="email" />
    </label>
    <label>
      Message
      <textarea formControlName="message" rows="5"></textarea>
    </label>
    <input formControlName="_gotcha" type="text" tabindex="-1" autocomplete="off" style="display: none" />
    <button type="submit" [disabled]="status() === 'sending'">
      {{ status() === 'sending' ? 'Sending...' : 'Send' }}
    </button>
    @if (status() === 'error') {
      <p role="alert">{{ error() }}</p>
    }
  </form>
}

Angular validates the fields before anything leaves the browser. The control names become the JSON keys, which are the names your answers are saved under. Signals update the view after the await, with or without Zone.js.

_gotcha is a hidden honeypot that only bots fill in, and _elapsed is how long the form was open. Nisuform drops submissions that trip either one and never saves the two fields.

To connect it, create a form, paste the key from its Setup tab into endpoint, and add ContactFormComponent to the imports of the page that renders <app-contact-form />. Run ng serve, wait two seconds and send a test. It shows up in your inbox and your email within seconds.

With HttpClient

If your app already provides HttpClient, you can use it instead of fetch. It turns a rejected submission into an error, so the message to show is on the error body:

With HttpClient
this.http
  .post<{ ok: boolean }>(endpoint, body, { headers: { Accept: 'application/json' } })
  .subscribe({
    next: () => this.status.set('sent'),
    error: (response) => {
      this.error.set(response.error?.message ?? 'Something went wrong. Please try again.')
      this.status.set('error')
    },
  })

Hosting

ng build outputs static files and the form posts from the browser, so Firebase Hosting, Netlify, Vercel, Cloudflare Pages and GitHub Pages all work. If your app sends a Content Security Policy, add https://api.nisuform.com to connect-src. When the site is live, add http://localhost:4200 and your domain to the form's allowed websites.

What you get with Nisuform

Free covers 100 submissions a month across 3 forms. Pro is $10 a month for 5,000 submissions and adds the features marked Pro.

  • Spam filtering on every plan

    A honeypot, a fill-time check, rate limits and duplicate checks run on every submission, with optional Cloudflare Turnstile or Google reCAPTCHA. Spam never counts toward your limit.

  • An email for every submission

    You get an email within seconds of each submission. Reply-to is set to the visitor, so you can answer from your mail app.

  • Every submission kept

    Search, star and export to CSV. Submissions stay until you delete them, so a missed email never loses a message.

  • File uploadsPro

    Up to 5 images or PDFs per submission, stored privately and shared through links that expire.

  • Slack, Discord, Sheets and webhooksPro

    Send each submission to Slack, Discord, Telegram or Google Sheets, or anywhere else with signed webhooks, Zapier or Make.

  • Auto-repliesPro

    Send each visitor a confirmation in your own words, from your own sender name.

Angular guides

Frequently asked questions

Can Angular send an email directly?

No. Angular runs in the visitor's browser, and browsers can't send email. You need server code with an email provider, or a form backend endpoint that receives the form, stores it and emails you.

Do I need HttpClient?

No. The component uses the browser's fetch, so you don't have to add provideHttpClient. HttpClient works too, but it reports a rejected submission as an error, so read the message from the error body.

Does it work with Angular SSR or prerendering?

Yes. The page can be rendered on the server or prerendered. The submit only runs in the browser after hydration, so the form doesn't depend on your server.

Does it work with older Angular versions?

The example uses standalone components, signals and the @if block, which need Angular 17 or later. On older versions, use an NgModule, class properties and *ngIf. The request to the endpoint stays the same.

Is it safe to put the endpoint in my Angular code?

Yes. The form key only accepts new submissions. Nobody can read your submissions or change the form with it. Add your domains to the form's allowed websites so other sites can't post to it.

Start collecting form submissions

Set up your forms with built-in spam protection in minutes for free.

Jane Smith

jane@smith.design

Live

12.4k

Submissions

24

Forms

0

Spam