Skip to content
~/mahadi hassan
← All posts

AWS in Production: Wiring S3, SES, SQS and EventBridge into NestJS

A healthcare platform leans on a lot of AWS. Here is how S3, SES, SQS, Rekognition and EventBridge Scheduler fit into a NestJS API — what each one is actually for, and how to keep them testable.

2 min read
~/mahadi/blog

AWS in Production: Wiring S3, SES, SQS and EventBridge into NestJS

AWSNestJS
On this page

EMPATHIKA — the enterprise healthcare platform I work on — leans on AWS for the heavy, undifferentiated work: storage, email, queues, image analysis, and scheduling. The trick is using each managed service for what it is genuinely good at, while keeping your own code from hard-coupling to the SDK everywhere. Here is how the pieces fit in a NestJS API.

S3: never let bytes touch your API

User uploads (documents, images) go straight to S3 via pre-signed URLs. The API mints a short-lived URL; the client uploads directly to S3; my server only stores the resulting object key. Large files never transit the NestJS process, so a 50 MB upload costs the API a few milliseconds, not a stalled request worker.

const url = await s3.getSignedUrlPromise('putObject', {
  Bucket: env.UPLOAD_BUCKET,
  Key: `patients/${id}/${randomUUID()}`,
  Expires: 60,
});
// client PUTs the file to `url`; we persist only the key

SES for email, SQS for everything that can wait

SES sends the transactional email (verification, alerts). But sending should never block the request that triggered it — so the work goes onto SQS. The API drops a message and returns immediately; a consumer drains the queue and calls SES. If SES is briefly throttled, messages wait in the queue instead of failing the user’s action. SQS also gives retries and a dead-letter queue for free.

Rekognition and EventBridge Scheduler

Rekognition handles image analysis (moderation, detection) without us training or hosting a model. EventBridge Scheduler runs the time-based work — reminders, periodic syncs — as managed cron, so there is no always-on timer process to babysit; AWS invokes the target when it is due.

Keep the SDK behind a boundary

The one rule that keeps all of this maintainable: no controller or domain service imports the AWS SDK directly. Each capability hides behind a small NestJS provider — StorageService, MailService, QueueService. Business code calls storage.put(...), not S3:

@Injectable()
export class StorageService {
  async signedUploadUrl(key: string) { /* ...S3 here only... */ }
}

That boundary means tests mock one interface instead of the SDK, and swapping or wrapping a service (add caching, change buckets) touches one file. It is the difference between using AWS and being married to it.

The throughline

Managed services are leverage: offload storage, email, queues, ML and scheduling to AWS and spend your time on the domain. Just keep the integration at the edges — direct-to-S3 uploads, async-via-SQS side effects, and a thin provider per service — and the platform stays fast, reliable, and testable.

Comments

Loading comments…