Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 5 min read

Add a drag and drop email builder to your Angular app (full tutorial)

Every SaaS eventually hears the same request: "Can we design our own emails inside your app?" Most teams either spend months building an editor, or pay per seat for one they can't control. In this post we add a complet

Every SaaS eventually hears the same request: "Can we design our own emails inside your app?"

Most teams either spend months building an editor, or pay per seat for one they can't control.

In this post we add a complete drag and drop email builder to a fresh Angular app, step by step. It's the written version of a 9 minute video where every step is filmed for real, nothing cut.

πŸŽ₯ Watch the video: https://www.linkedin.com/feed/update/urn:li:activity:7519630925253615616

What you need: an Angular 17 to 22 app (ours is a fresh ng new on Angular 22) and Node 18 or newer.

1. Create an account and a token

Sign up at mailkaro.com. Sign in is passwordless: every sign in uses a one time code sent to your email, and an account can only be signed in on one device at a time.

Then open Embed & API, click Generate new token, and choose which features the token exposes, for example:

  • Save, export and history
  • Uploads and assets
  • AI Assistant

You get two keys:

Key Starts with Where it goes
Publishable pk_live_ your frontend
Secret sk_live_ your server only, never in frontend code

2. Install and render

npm install angular-email-craft

Put the publishable key in src/environments/environment.ts:

export const environment = {
  mailkaroKey: "pk_live_...",
};

Then render the editor. It's one standalone component:

import { Component } from "@angular/core";
import { EmailEditorComponent } from "angular-email-craft";
import { environment } from "../environments/environment";

@Component({
  selector: "app-root",
  imports: [EmailEditorComponent],
  template: `<email-editor [token]="token" minHeight="100vh" />`,
})
export class App {
  token = environment.mailkaroKey;
}

That's the full editor: the canvas in the middle, blocks and settings on the right.

Tip: the starter app.html is a placeholder page. Use the inline template above (or empty app.html) so the editor gets the whole page.

NgModule app? Import EmailEditorModule from angular-email-craft instead.

3. Save and reopen a design

The (ready) output hands you the editor. Add two buttons. exportHtml gives you the design (JSON describing everything on the canvas) and the HTML. Store the design: that's what reopens the email later.

import { Component } from "@angular/core";
import { EmailEditorComponent, type EditorApi } from "angular-email-craft";
import { environment } from "../environments/environment";

@Component({
  selector: "app-root",
  imports: [EmailEditorComponent],
  template: `
    <button (click)="save()">Save</button>
    <button (click)="open()">Open</button>
    <email-editor [token]="token" minHeight="90vh" (ready)="editor = $event" />
  `,
})
export class App {
  token = environment.mailkaroKey;
  editor?: EditorApi;

  save() {
    this.editor?.exportHtml(({ design, html }) => {
      localStorage.setItem("design", JSON.stringify(design)); // your database in a real app
    });
  }

  open() {
    const saved = localStorage.getItem("design");
    if (saved) this.editor?.loadDesign(JSON.parse(saved));
  }
}

Called without a callback, exportHtml() returns a promise, so const { design, html } = await this.editor!.exportHtml() works too.

4. Merge tags

Each merge tag has a label, the value your email service fills in, and a sample:

mergeTags = {
  first_name: { label: "First name", value: "{{first_name}}", sample: "Alex" },
  company: { label: "Company", value: "{{company}}", sample: "Acme" },
};
<email-editor [token]="token" [mergeTags]="mergeTags" minHeight="90vh" (ready)="editor = $event" />

In a text block, your users pick Insert merge tag. The tag lands as a labelled pill, so they can see exactly where each field goes.

5. Render email-safe HTML on your server

Sending needs finished HTML, and you can make it on your own server from the saved design. In a server folder next to your app:

npm install email-craft-sdk express
// server/index.mjs
import express from "express";
import { renderDesign, renderMjml } from "email-craft-sdk";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/render", (req, res) => {
  const html = renderDesign(req.body.design, { fullDocument: true });
  const mjml = renderMjml(req.body.design);
  res.json({ html, mjml });
});

app.listen(3001, () => console.log("Server on http://localhost:3001"));

There's no headless browser, no network call, and no key needed. It runs inside your backend.

6. Decide what each user can do

The plan unlocks when both keys work together: the publishable key in the browser, and the secret key on your server. Add one route:

import { Mailkaro } from "email-craft-sdk";

const mailkaro = Mailkaro.fromEnv(); // reads MAILKARO_SERVER_KEY (sk_live_...)

app.get("/mailkaro/embed-runtime", async (_req, res) => {
  res.json(await mailkaro.resolveRuntime());
});

Point the editor at it with runtimeUrl:

<email-editor
  [token]="token"
  runtimeUrl="/mailkaro/embed-runtime"
  [mergeTags]="mergeTags"
  minHeight="90vh"
  (ready)="editor = $event" />

In development, let the Angular dev server forward these routes to Node, so they run on the same origin as your app. Create proxy.conf.json:

{
  "/mailkaro": { "target": "http://localhost:3001" },
  "/render": { "target": "http://localhost:3001" }
}

and point ng serve at it in angular.json (under serve):

"options": { "proxyConfig": "proxy.conf.json" }

Your server answers, and the secret key never leaves it.

7. Your own storage and your own AI

Add your S3 bucket and AI provider key to the server's .env, then three small routes:

import multer from "multer";

app.post("/mailkaro/ai", async (req, res) => {
  res.json(await mailkaro.ai(req.body.method, req.body.input));
});

const upload = multer();
app.post("/mailkaro/upload", upload.single("file"), async (req, res) => {
  res.json(await mailkaro.uploadAsset({
    buffer: req.file.buffer,
    filename: req.file.originalname,
    contentType: req.file.mimetype,
    scopeId: "demo", // your tenant or workspace id
  }));
});

app.get("/mailkaro/assets", async (_req, res) => {
  res.json(await mailkaro.listAssets({ scopeId: "demo" }));
});

Now the image library has an Amazon S3 tab with your bucket's images, and new uploads go straight into it. The AI assistant runs on your own key and builds emails right on the canvas. The keys stay on the server, and the browser never sees them.

Already on angular-email-editor?

Install angular-email-craft and change the import:

// before
import { EmailEditorComponent } from "angular-email-editor";
// after
import { EmailEditorComponent } from "angular-email-craft";

Your template, your (loaded) / (ready) handlers and your exportHtml / loadDesign calls keep working, and every design you saved with Unlayer opens as it is.

The whole loop

  • One token
  • One standalone component
  • Two calls to save and reopen
  • One function to render on your server
  • One route that decides what each user can do

The free plan covers the editor itself. Pro unlocks storage, AI and more, and the first month is free with no card.

Next in the series: the same setup in Vue and plain JavaScript.

Which one does your product need first: the email editor, or the AI writing the emails? Tell me in the comments.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.