Guides

Angular contact form that sends email (no backend)

A standalone Angular component with reactive forms, validation and loading states that posts to FormSMTP with HttpClient. No Node server or cloud function to run.

Last updated November 2, 2026

Written for Angular 22, the current version on angular.dev. It uses inject(), signals and the @if block, all available since Angular 17.

1Create a FormSMTP project

Sign up free and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click Test connection. Not sure which provider? Your existing mailbox works: see the Gmail or Zoho guides.

2Lock the project to your site

In Settings → Security, set Allowed websites to https://yourapp.com and Allowed recipients to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.

ng serve runs on http://localhost:4200. Add it to Allowed websites while you develop, or use a separate FormSMTP project for development.

3Put the endpoint in environment.ts

If your project has no environment files yet, run ng generate environments. It creates environment.ts (production) and environment.development.ts, and sets up the file replacement in angular.json.

ts
// src/environments/environment.ts
export const environment = {
  formsmtpEndpoint: "https://app.formsmtp.com/api/v1/form/YOUR_FORM_KEY",
};

Put the same line, with a development key if you like, in environment.development.ts. Always import the base file; the CLI swaps in the right one when it builds.

4Make HttpClient available

In Angular 21 and later, HttpClient can be injected without any setup. On earlier versions, add it to your app config:

ts
// src/app/app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideHttpClient } from "@angular/common/http";

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

5Create the component

ts
// src/app/contact-form/contact-form.ts
import { Component, inject, signal } from "@angular/core";
import { HttpClient, HttpErrorResponse, HttpParams } from "@angular/common/http";
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { environment } from "../../environments/environment";

type FormReply = { success: boolean; error?: string };

@Component({
  selector: "app-contact-form",
  imports: [ReactiveFormsModule],
  templateUrl: "./contact-form.html",
})
export class ContactForm {
  private http = inject(HttpClient);
  private loadedAt = Date.now();

  protected status = signal<"idle" | "sending" | "sent" | "error">("idle");
  protected error = signal("");

  protected form = inject(NonNullableFormBuilder).group({
    name: ["", Validators.required],
    email: ["", [Validators.required, Validators.email]],
    message: ["", [Validators.required, Validators.maxLength(5000)]],
    _gotcha: [""], // honeypot: stays empty for people
  });

  protected submit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    this.status.set("sending");
    const body = new HttpParams({
      fromObject: { ...this.form.getRawValue(), _subject: "New message from the website", _t: this.loadedAt },
    });
    this.http
      .post<FormReply>(environment.formsmtpEndpoint, body, { headers: { Accept: "application/json" } })
      .subscribe({
        next: (reply) => {
          if (!reply.success) return this.fail(reply.error);
          this.status.set("sent");
          this.form.reset();
        },
        error: (err: HttpErrorResponse) => this.fail(err.error?.error),
      });
  }

  private fail(message?: string) {
    this.error.set(message ?? "Something went wrong. Please try again.");
    this.status.set("error");
  }
}
html
<!-- src/app/contact-form/contact-form.html -->
@if (status() === "sent") {
  <p>Thanks! We'll get back to you soon.</p>
} @else {
  <form [formGroup]="form" (ngSubmit)="submit()">
    <label>Name <input formControlName="name" autocomplete="name" /></label>
    @if (form.controls.name.touched && form.controls.name.invalid) {
      <small>Please enter your name.</small>
    }
    <label>Email <input formControlName="email" type="email" autocomplete="email" /></label>
    @if (form.controls.email.touched && form.controls.email.invalid) {
      <small>Please enter a valid email address.</small>
    }
    <label>Message <textarea formControlName="message" rows="5"></textarea></label>
    <input formControlName="_gotcha" style="display: none" tabindex="-1" autocomplete="off" />

    <button type="submit" [disabled]="status() === 'sending'">
      {{ status() === "sending" ? "Sending…" : "Send" }}
    </button>
    @if (status() === "error") {
      <p role="alert">{{ error() }}</p>
    }
  </form>
}

Components are standalone by default in current Angular, so the component imports ReactiveFormsModule itself. Use it anywhere with <app-contact-form /> after adding ContactForm to the parent component’s imports.

Why HttpParams?

Passing HttpParams as the body makes Angular send the fields URL-encoded, like a normal HTML form. That’s a “simple” cross-origin request, so the browser skips the CORS preflight. The Accept: application/json header tells FormSMTP to answer with { success, error } instead of a thank-you page. _t is when the component loaded; FormSMTP drops submissions made within two seconds of it, which is how bots behave.

Prefer JSON? Pass a plain object instead and Angular sends application/json. The browser then sends a preflight first, which FormSMTP answers, so both work:

ts
this.http.post<FormReply>(environment.formsmtpEndpoint, {
  ...this.form.getRawValue(),
  _subject: "New message from the website",
  _t: this.loadedAt,
});

Don’t post a FormData object. Angular sends it as multipart/form-data, which the form endpoint rejects with “File uploads are not supported”.

Server-side rendering and prerendering

Nothing changes with Angular SSR or prerendering. The component renders on the server, but submit() only runs in the browser, which posts straight to FormSMTP. You don’t need an Express route or a Firebase function.

Troubleshooting

  • NullInjectorError: No provider for HttpClient! You’re on Angular 20 or earlier without provideHttpClient() in app.config.ts.
  • 403 “This form isn’t allowed on http://localhost:4200”: add the dev origin to Allowed websites, or use a development project.
  • “Http failure response for … 0 Unknown Error”: the request never got a response. Check the endpoint URL in environment.ts, and whether a browser extension blocked it.
  • 415 “File uploads are not supported”: the body was a FormData object. Use HttpParams or a plain object.
  • 429 “Too many submissions”: one visitor sent several messages in a short time. The limit protects you from floods; wait a few minutes.

FAQ

Is it safe to put the key in environment.ts?

Yes, as long as it’s the form key (fk_). Everything in environment.ts ends up in your JavaScript bundle, and the form key is made for that: it only emails you, and you can lock it to your domain. Never put the API key (fs_) there.

Do I need a backend or Firebase function?

No. The browser posts to FormSMTP over HTTPS, and FormSMTP sends through your SMTP account. Your Angular app can be served as static files from any host.

Can I use template-driven forms or Signal Forms?

Yes. The form API only changes how you collect and validate the fields. The HttpClient call that sends them is the same.

Why did my message land in spam?

Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.

More guides

Get a Founding Pass

10,000 emails a month, free for your first year. Pass No. 0008 is next: 1,993 of 2,000 left, no card.