Skip to content
← All frameworks
ts · Angular

TypeScript framework

Angular form handling without a backend

An Angular app is a frontend. When it needs a contact form, the usual next step is an Express or serverless endpoint that receives the post, sends an email and stores the message. You can skip that step: the component posts straight to your Formward endpoint, which stores the submission in Sweden, filters spam and emails you.

The example below is a standalone component with a typed reactive form, HttpClient and signals. The honeypot is an ordinary form control that real visitors never see, so it travels with the rest of the values. The template uses the @if block syntax from Angular 17 onward.

EU-hosted · Your Angular app can be served from any CDN while the form data it collects is received and stored by Formward in Sweden.

The Angular example

Copy this into your project and replace <FORM_ID> with the id of a form you create in the Formward dashboard.

ts

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

const ENDPOINT = "https://forms.formward.eu/f/<FORM_ID>";

@Component({
  selector: "app-contact-form",
  imports: [ReactiveFormsModule],
  template: `
    @if (status() === "ok") {
      <p>Thanks, we will reply by email.</p>
    } @else {
      <form [formGroup]="form" (ngSubmit)="submit()">
        <label>Name <input formControlName="name" /></label>
        <label>Email <input type="email" formControlName="email" /></label>
        <label>Message <textarea formControlName="message"></textarea></label>
        <!-- Honeypot: hidden from people, filled by bots. -->
        <input type="text" formControlName="_gotcha" tabindex="-1" autocomplete="off"
          aria-hidden="true" style="position:absolute;left:-5000px" />
        <button type="submit" [disabled]="form.invalid || status() === 'sending'">
          {{ status() === "sending" ? "Sending..." : "Send" }}
        </button>
        @if (error()) {
          <p role="alert">{{ error() }}</p>
        }
      </form>
    }
  `,
})
export class ContactForm {
  private http = inject(HttpClient);
  private fb = inject(NonNullableFormBuilder);

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

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

  submit() {
    if (this.form.invalid) return;
    this.status.set("sending");
    this.error.set("");

    const body = new FormData();
    for (const [key, value] of Object.entries(this.form.getRawValue())) {
      body.append(key, value);
    }

    this.http
      .post<{ ok: boolean; id?: string }>(ENDPOINT, body, {
        headers: { Accept: "application/json" },
      })
      .subscribe({
        next: () => {
          this.form.reset();
          this.status.set("ok");
        },
        error: (err: HttpErrorResponse) => {
          this.error.set(
            err.status === 429
              ? "Too many attempts. Wait a minute and try again."
              : "Could not send. Please try again.",
          );
          this.status.set("idle");
        },
      });
  }
}

Success and error handling

  • HttpClient sends a FormData body as multipart/form-data and lets the browser set the boundary, so do not set a Content-Type header yourself.
  • A 4xx reply arrives in the error callback as an HttpErrorResponse; err.error holds Formward's { ok: false, error } body (for a 422 it also names the field), and err.status tells you which case you hit.
  • On Angular 17 or 18, add standalone: true to the @Component decorator. From Angular 19 it is the default.
  • Prefer a JSON body? Post this.form.getRawValue() directly. Formward accepts JSON too; the browser then sends a CORS preflight first, which the endpoint answers.

Register HttpClient once

Standalone apps provide HttpClient in app.config.ts. Add it next to the providers you already have, such as provideRouter. withFetch() makes HttpClient use the browser's fetch, which Angular recommends when the app also renders on the server.

ts

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

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

Spam protection

Every example above includes the _gotcha honeypot field. It is hidden from real users and must stay empty. When a bot fills it, Formward files the submission as spam and still answers with a normal success response, so the bot learns nothing and nothing reaches your inbox. Rate limits and the origin allowlist are checked on Formward's side as well. For a stricter gate, switch on Cloudflare Turnstile in the form's settings and add the widget; it adds a cf-turnstile-response field that Formward verifies on receipt. Turnstile is optional, and Cloudflare is a US provider, listed as such in the sub-processor register. The spam docs have the widget markup.

The JSON response

A standard POST gets a 302 redirect (or your thank-you page). Send the Accept: application/json header - as every fetch() example above does - and Formward returns JSON instead:

HTTP/1.1 200 OK
Content-Type: application/json

{ "ok": true, "id": "3f2a9c1e-...", "files": 0, "success": { "title": "Thank you!", ... } }

files is the number of uploaded files stored and success carries the confirmation text from the form's settings. When you set a redirect URL for the form, the reply also has a redirect field your code can send the visitor to. On failure the body is { ok: false, error } with a 4xx status: 422 when required consent is missing or a field is too long, 403 when a Turnstile check fails, 429 for too many submissions from one address, 402 when the account is over its plan limit. See responses and redirects for the full table and the AJAX docs for CORS.

Other frameworks

Collect your first Angular submission

Create a form, paste the snippet, and keep every submission in the EU. GDPR-clean from the first POST.

Angular contact form without a backend | Formward