Queue-Backed Email API
Build an email sending service in JavaScript: other applications call its API to send an email (such as a welcome message or a password reset), the API answers immediately with a job id, and background workers send the emails through SMTP with Nodemailer, retrying temporary failures (such as a server busy error) with increasing waits and giving up on permanent ones (such as an invalid address). Jobs are stored durably, so nothing is lost when the service restarts, sending rates are limited so you don't get blocked, and a React dashboard built with Vite shows each email's status. Use a test SMTP server such as Mailpit while developing. You'll learn job queues, retries and idempotency, rate limiting, classifying errors, and building an operations dashboard.
- Estimate
- ~13.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
Node.js · Nodemailer · SQLite · Vite · React
Project roadmap
- 01
Design the service
~2hWrite DESIGN.md: an email job (id, idempotency key from the caller, to, subject, text and HTML body or a template name with data, status queued, sending, sent, failed or dead, attempts, next attempt time and last error), how jobs are stored (SQLite or PostgreSQL) and claimed by workers, which SMTP errors are temporary (4xx codes and timeouts) and which are permanent (5xx), the rate limit, and the API. Done when: DESIGN.md covers jobs, states, errors and limits.
- 02
Build the API
~2hBuild a Node.js API (with Express or Fastify) with POST /emails, validating the input (valid addresses, subject length and body size) and an API key, storing the job and returning 202 with its id; a repeated idempotency key returns the existing job. Add GET /emails/:id and GET /emails with filters. Done when: emails can be queued and their status read, and duplicates are recognized.
- 03
Send with workers and retries
~2.5hWrite a worker that claims due jobs one at a time (in a transaction, marking them sending with a lease), renders templates, sends with Nodemailer through the configured SMTP server, and marks jobs sent with the message id. On temporary errors, schedule a retry with exponential backoff and jitter; on permanent errors or after max attempts, mark the job dead with the reason. Test with Mailpit running in Docker. Done when: emails arrive in Mailpit, and simulated failures are retried or marked dead correctly.
- 04
Survive restarts and limit the rate
~2hOn startup, return jobs stuck in sending with an expired lease to the queue. Limit sending to N emails per second overall and per recipient domain (with a token bucket), and run a configurable number of workers. Make sure a job is never marked sent twice or sent twice after a crash as far as possible, and document the remaining risk (the email sent but the process crashing before recording it). Done when: killing the service mid-sending loses no jobs, and the rate limit holds.
- 05
Build the dashboard
~2hBuild a React dashboard with Vite showing counts per status, a searchable list of emails with their status, attempts and last error, the details of one email with its full history, and buttons to retry dead emails. Protect it with a login or the API key. Done when: the dashboard shows live statuses and can retry dead emails.
- 06
Add observability
~1hLog each attempt with the job id, attempt number, duration and result (never the email body), and expose metrics: emails queued, sent and failed per minute, the queue backlog and the age of the oldest queued email. Done when: the metrics show the service's health.
- 07
Test it
~2hWrite tests with a fake transport (or Mailpit's API): validation, idempotency, success, temporary and permanent failures, retries with backoff (using fake timers), recovery after a restart, and rate limiting. Done when: all tests pass.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack