Angular Contact Form - Setup Guide with FormWit

Last updated: March 2026

Building an Angular contact form that sends emails typically means setting up a Node.js backend, configuring an email service, and wiring up API endpoints. With a form backend, you skip all of that. This guide shows how to add a fully functional contact form to any Angular project using reactive forms and FormWit.

How it works

Your Angular component renders a form using reactive forms. When a visitor submits it, the component sends the data to a FormWit endpoint via Angular's HttpClient. FormWit validates the submission, filters spam, stores it in your dashboard, and sends you an email notification. No Express server, no email service, no database.

The flow:

  1. A visitor fills out the contact form in your Angular app
  2. The component sends the form data to your FormWit endpoint via HttpClient
  3. FormWit validates the data, checks for spam, and stores the submission
  4. You receive an email notification and can view the submission in your dashboard

Set up an Angular contact form

Step 1: Create a FormWit account

Go to app.formwit.com/auth/signup and create a free account. No credit card required. Click Create Form, name it (e.g., "Angular Contact Form"), and copy the endpoint URL. It looks like https://app.formwit.com/api/s/YOUR_FORM_ID.

Step 2: Create the contact form component

Generate a new component using the Angular CLI:

ng generate component contact-form

This creates four files. You will edit the TypeScript and HTML files. The component uses Angular's ReactiveFormsModule for form handling and HttpClient to submit data.

contact-form.component.ts

import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  ReactiveFormsModule,
  FormBuilder,
  FormGroup,
  Validators,
} from '@angular/forms';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: './contact-form.component.html',
  styleUrl: './contact-form.component.css',
})
export class ContactFormComponent {
  private fb = inject(FormBuilder);
  private http = inject(HttpClient);

  readonly FORM_URL = 'https://app.formwit.com/api/s/YOUR_FORM_ID';

  status: 'idle' | 'sending' | 'success' | 'error' = 'idle';

  contactForm: FormGroup = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    message: ['', Validators.required],
    _gotcha: [''],
  });

  onSubmit(): void {
    if (this.contactForm.invalid) {
      this.contactForm.markAllAsTouched();
      return;
    }

    this.status = 'sending';

    const formData = new FormData();
    Object.entries(this.contactForm.value).forEach(([key, value]) => {
      formData.append(key, value as string);
    });

    this.http.post(this.FORM_URL, formData).subscribe({
      next: () => {
        this.status = 'success';
        this.contactForm.reset();
      },
      error: () => {
        this.status = 'error';
      },
    });
  }
}

contact-form.component.html

<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
  <div class="field">
    <label for="name">Name</label>
    <input type="text" id="name" formControlName="name" />
    <span
      class="error-text"
      *ngIf="contactForm.get('name')?.touched && contactForm.get('name')?.invalid"
    >
      Name is required.
    </span>
  </div>

  <div class="field">
    <label for="email">Email</label>
    <input type="email" id="email" formControlName="email" />
    <span
      class="error-text"
      *ngIf="contactForm.get('email')?.touched && contactForm.get('email')?.invalid"
    >
      Please enter a valid email address.
    </span>
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" formControlName="message" rows="5"></textarea>
    <span
      class="error-text"
      *ngIf="contactForm.get('message')?.touched && contactForm.get('message')?.invalid"
    >
      Message is required.
    </span>
  </div>

  <!-- Honeypot spam protection -->
  <input
    type="text"
    formControlName="_gotcha"
    style="display:none"
    tabindex="-1"
    autocomplete="off"
  />

  <button type="submit" [disabled]="status === 'sending'">
    {{ status === 'sending' ? 'Sending...' : 'Send Message' }}
  </button>

  <p class="success-text" *ngIf="status === 'success'">
    Message sent successfully!
  </p>
  <p class="error-text" *ngIf="status === 'error'">
    Something went wrong. Please try again.
  </p>
</form>

Replace YOUR_FORM_ID with the endpoint ID from your FormWit dashboard. The component uses FormData to serialize all fields and sends them via HttpClient.post(). The hidden _gotcha field is a honeypot: bots fill it in, real visitors never see it, and FormWit uses it to filter spam.

Step 3: Provide HttpClient

Angular requires HttpClient to be provided at the application level. If you are using Angular 17+ with standalone components, add provideHttpClient() to your app config:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

If you use the older NgModule pattern, import HttpClientModule in your root module instead.

Step 4: Add the component to a page

Use the component in any template. If you have a contact page, import and use it there:

// contact.component.ts
import { Component } from '@angular/core';
import { ContactFormComponent } from '../contact-form/contact-form.component';

@Component({
  selector: 'app-contact',
  standalone: true,
  imports: [ContactFormComponent],
  template: `
    <h1>Contact Us</h1>
    <p>Have a question? Fill out the form below.</p>
    <app-contact-form />
  `,
})
export class ContactComponent {}

Add the route in your router configuration:

// app.routes.ts
import { Routes } from '@angular/router';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: 'contact', component: ContactComponent },
];

Step 5: Test it

Start the Angular dev server:

ng serve

Navigate to /contact, fill in the fields, and click "Send Message." The success message should appear. Check your FormWit dashboard for the submission and your email inbox for the notification.

Template-driven forms alternative

Angular supports two form patterns: reactive forms and template-driven forms. The reactive approach above is recommended for most projects. If you prefer template-driven forms, here is the same component using ngModel:

// contact-form.component.ts (template-driven)
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './contact-form.component.html',
})
export class ContactFormComponent {
  private http = inject(HttpClient);

  readonly FORM_URL = 'https://app.formwit.com/api/s/YOUR_FORM_ID';

  model = { name: '', email: '', message: '', _gotcha: '' };
  status: 'idle' | 'sending' | 'success' | 'error' = 'idle';

  onSubmit(): void {
    this.status = 'sending';

    const formData = new FormData();
    Object.entries(this.model).forEach(([key, value]) => {
      formData.append(key, value);
    });

    this.http.post(this.FORM_URL, formData).subscribe({
      next: () => {
        this.status = 'success';
        this.model = { name: '', email: '', message: '', _gotcha: '' };
      },
      error: () => {
        this.status = 'error';
      },
    });
  }
}
<!-- Template-driven version -->
<form #contactForm="ngForm" (ngSubmit)="onSubmit()">
  <div class="field">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" [(ngModel)]="model.name" required />
  </div>

  <div class="field">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" [(ngModel)]="model.email" required />
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" name="message" [(ngModel)]="model.message" rows="5" required></textarea>
  </div>

  <input type="text" name="_gotcha" [(ngModel)]="model._gotcha"
    style="display:none" tabindex="-1" autocomplete="off" />

  <button type="submit" [disabled]="status === 'sending'">
    {{ status === 'sending' ? 'Sending...' : 'Send Message' }}
  </button>

  <p class="success-text" *ngIf="status === 'success'">Message sent successfully!</p>
  <p class="error-text" *ngIf="status === 'error'">Something went wrong. Please try again.</p>
</form>

Both approaches send identical data to FormWit. Reactive forms give you more control over validation and are easier to test. Template-driven forms require less TypeScript code and work well for simpler forms.

Form validation details

The reactive forms example includes built-in Angular validators:

  • Validators.required prevents submission with empty fields
  • Validators.email validates the email format client-side
  • markAllAsTouched() shows validation errors when the user tries to submit an invalid form

FormWit also validates submissions server-side. If a required field is missing, the API returns an error. Client-side validation improves the user experience by catching issues before the network request.

To add custom validators (like minimum message length), extend the form group:

contactForm: FormGroup = this.fb.group({
  name: ['', [Validators.required, Validators.minLength(2)]],
  email: ['', [Validators.required, Validators.email]],
  message: ['', [Validators.required, Validators.minLength(10)]],
  _gotcha: [''],
});

Adding a custom redirect

To redirect visitors to a thank-you page after submission, handle the redirect in your component:

import { Router } from '@angular/router';

private router = inject(Router);

// Inside the subscribe next callback:
this.http.post(this.FORM_URL, formData).subscribe({
  next: () => {
    this.router.navigate(['/thank-you']);
  },
  error: () => {
    this.status = 'error';
  },
});

This uses Angular Router for client-side navigation without a full page reload.

Why not build your own API?

Angular apps often pair with a Node.js or .NET backend. You could build your own /api/contact endpoint. But that means:

  • Setting up and maintaining a server process
  • Configuring an email service (SendGrid, Resend, Mailgun)
  • Managing API keys and environment variables
  • Writing validation, sanitization, and error handling
  • Building a spam filter from scratch
  • No built-in dashboard to view or search submissions

A form backend handles all of this. Your Angular app stays purely client-side, and you can deploy it to any static hosting provider (Firebase Hosting, Vercel, Netlify, AWS S3 + CloudFront).

Summary

Adding a contact form to an Angular app does not require a backend server or email configuration. With FormWit, you create a form endpoint, build a standard Angular component using reactive forms or template-driven forms, and let the service handle spam filtering, data storage, and email notifications. The component works with any Angular version from 14+ and supports both standalone and NgModule architectures.

FormWit's free plan includes unlimited forms, 100 submissions per month, email notifications, and built-in spam protection. Create your free account and add a contact form to your Angular app today.

Related guides: React contact form · Vue contact form · JavaScript contact form · HTML contact form · Spam protection · Contact form templates

Want to skip the setup?

FormWit gives you a form endpoint in 60 seconds. Free plan, no credit card.

Create Free Form

Need a form fast?

Build one visually with our free HTML form generator — no coding required.

Try the Form Generator →

Add FormWit to your Angular app

Add a contact form to your site in 30 seconds. No backend code required.

Try FormWit Free