Skip to main content
New:Build with AI agents

Angular contact forms
without a mail server.

Use Reactive Forms and HttpClient for the UI while Form Plume handles notifications, stored submissions, spam protection, file uploads, integrations, and signed webhooks.

Free foreverNo credit cardAngular endpoint ready in under a minute
contact-form.component.ts
submit() {
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    return;
  }

  const body = new FormData();

  Object.entries(this.form.getRawValue()).forEach(([key, value]) => {
    body.append(key, value);
  });

  this.http.post(FORM_PLUME_ENDPOINT, body).subscribe();
}

The full guide

A hosted backend for Angular contact forms.

Wire Reactive Forms or template-driven forms into Form Plume without adding a Node, NestJS, SMTP, or database layer.

  1. 1
    Create your Form Plume accountStart free and give your Angular form a hosted endpoint for email, submissions, spam filtering, uploads, integrations, and webhooks.Start free
  2. 2
    Create the form and copy the endpointCopy the endpoint URL from Form Plume. It looks like https://api.formplume.com/f/your-slug
  3. 3
    Connect it inside AngularCreate a Reactive Form, convert the values to FormData, and submit them with HttpClient. Send one test submission from the real Angular app or preview environment and confirm it reaches Form Plume.

You can use this AI prompt to hand the platform-specific wiring to an assistant without accidentally creating a backend.

Reactive Forms setup

Reactive Forms are a clean default when an Angular contact form needs validation and submission state.

// contact-form.component.ts
import { Component } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { HttpClient } from "@angular/common/http";
 
const FORM_PLUME_ENDPOINT = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE";
 
@Component({
  selector: "app-contact-form",
  templateUrl: "./contact-form.component.html",
})
export class ContactFormComponent {
  status: "idle" | "sending" | "sent" | "error" = "idle";
 
  form = this.fb.nonNullable.group({
    name: ["", Validators.required],
    email: ["", [Validators.required, Validators.email]],
    message: ["", Validators.required],
  });
 
  constructor(
    private fb: FormBuilder,
    private http: HttpClient,
  ) {}
 
  submit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
 
    this.status = "sending";
    const body = new FormData();
 
    Object.entries(this.form.getRawValue()).forEach(([key, value]) => {
      body.append(key, value);
    });
 
    this.http.post(FORM_PLUME_ENDPOINT, body).subscribe({
      next: () => {
        this.status = "sent";
        this.form.reset();
      },
      error: () => {
        this.status = "error";
      },
    });
  }
}
<!-- contact-form.component.html -->
<form [formGroup]="form" (ngSubmit)="submit()">
  <label for="name">Name</label>
  <input id="name" formControlName="name" autocomplete="name" />
 
  <label for="email">Email</label>
  <input id="email" type="email" formControlName="email" autocomplete="email" />
  <p *ngIf="form.controls.email.touched && form.controls.email.invalid" role="alert">
    Enter a valid email address.
  </p>
 
  <label for="message">Message</label>
  <textarea id="message" formControlName="message" rows="5"></textarea>
 
  <button type="submit" [disabled]="status === 'sending'">
    {{ status === "sending" ? "Sending..." : "Send message" }}
  </button>
 
  <p *ngIf="status === 'sent'" role="status">Thanks, we got your message.</p>
  <p *ngIf="status === 'error'" role="alert">Please try again.</p>
</form>

Form Plume receives the Angular submission and handles the backend pipeline: email notifications, stored submissions, spam filtering, uploads, integrations, and signed webhooks.

File input handling

File inputs are not stored naturally in a text form group. Read the selected file and append it to FormData.

<label for="attachment">Attachment</label>
<input id="attachment" type="file" (change)="selectFile($event)" />
selectedFile?: File;
 
selectFile(event: Event) {
  const input = event.target as HTMLInputElement;
  this.selectedFile = input.files?.[0];
}
 
// inside submit()
if (this.selectedFile) {
  body.append("attachment", this.selectedFile);
}

Do not JSON-stringify files. Let the browser build the multipart request.

Template-driven alternative

For a small Angular form, template-driven forms also work. The important part is that every control has a name, and the final submit sends named fields to Form Plume.

Use Reactive Forms when you need typed values, reusable validators, or more predictable testing.

Angular accessibility notes

Connect errors to fields with aria-describedby when messages are outside the label.

Mark touched controls before showing validation errors so visitors are not greeted by a page full of alerts.

Keep the status message in the template, not only in a toast. Screen reader users need the result near the form.

You do not need a NestJS or Express endpoint just to receive the Angular form. Form Plume is the hosted form backend.

Primary sources

FAQ

Angular form questions
before release.

Reactive Forms are a strong default because validation, touched state, and submit behavior are explicit. Template-driven forms also work for small forms.

One line. Zero backend.

The form backend you don’t have to build.

Free foreverNo credit cardSet up in under a minute