The job EmailJS does
A static site needs a contact form; a contact form needs to send email; email needs a server you don't have. EmailJS bridges that: your front-end calls their API with a public key, their servers send through your connected mail account. For a portfolio site's contact form, that's the whole job, done.
Where client-side sending stops
Everything in the browser is public: your template IDs, your public key, your rate limits, all inspectable and scriptable by anyone who opens devtools. There's no way to send transactional email safely (a password reset triggered purely client-side is an attacker's toy), no delivery events you can trust, and the free tier caps fast.
The pattern that scales keeps the form client-side and moves the send server-side: the form captures the contact against a public endpoint, and the email fires from the platform, where secrets live and events flow.
We built that public endpoint the paranoid way, and it shows what client-side surfaces demand: it always returns the same success response whether or not the contact exists, so nobody can use your form to enumerate email addresses, and it's idempotent, so double-submits change nothing. That is the level of thinking your contact form needs, and it's exactly what a client-side sending key can never give you.
The paranoia is earned. In June a bot wave signing up from one disposable domain pushed about 6,400 phishing sends through roughly 40 throwaway accounts before we closed the gap. The fix added a disposable and blocked email-domain trigger to the signup gate, with full-domain and dot-boundary subdomain matching against a curated blocklist, and IP capture on every account-creation path including OAuth. Public-facing forms attract exactly this traffic, which is why ours fail closed.
We ship a browser-safe public client for exactly this pattern, @nitrosend/sdk/browser, so the front-end capture is typed and clean while every send stays server-side. And the loop it feeds is real: one of our customers wired their website contact form into a three-email welcome flow, built by asking Claude, in one instruction. Form captures, flow nurtures, nothing sent from the browser.
The mechanics under that are worth spelling out. The signup attaches the contact to a chosen list, and the list membership itself fires the welcome flow. No server of your own, no secret key exposed anywhere, and the same outcome the client-side sending approach was reaching for.
The key design enforces the boundary mechanically. Server code holds a secret key. The browser package takes only a public website key, requires an origins allowlist so another site can't lift the key and reuse it, and is hardcoded to refuse a secret key outright if someone tries to initialize it with one. The safe pattern shouldn't depend on developers reading the docs. It should be the only pattern that runs.
Full parameters and responses live in the REST API docs and the API reference.
Go deeper
The server-side send is one call from Node.js or TypeScript, and serverless functions mean "a backend" is ten lines on Vercel or Cloudflare. A server-side API key unlocks the full account surface, and it belongs in that function's environment, never in the bundle. OAuth is the other route worth knowing about, since most of our clients support signing in through the browser without an API key at all. The server-side fix is the same one every sender ends up at: SPF, DKIM and DMARC authentication, backed by how to set up SMTP so the domain is provably yours.