AWS
Workload-first cloud architecture: serverless, storage, data, security and cost.
- Serverless
- S3 & DynamoDB
- IAM
- Cost awareness
I use Node.js for APIs, SaaS backends, integrations, automation and serverless workloads, especially when asynchronous processing and cloud services are central to the application.
API architecture · Async patterns · Serverless · Observability
How I think about it
Node.js is a strong fit for I/O-heavy applications and integration-heavy systems, but good architecture still requires clear boundaries, careful async handling, validation, observability and protection against long-running or blocking work.
Key experience
Scope
Deep dive
I keep three layers distinct: routes and controllers that translate HTTP into calls and back, services that hold the business workflow, and data access that knows about the database. Express doesn't enforce any of this, which is why so many Node codebases end up with SQL inside route handlers.
Errors flow through one handling path that maps known error types to status codes and one response shape. Unknown errors become a generic 500 with the details in the logs, not in the response.
async/await makes asynchronous code read sequentially, which hides an easy performance mistake: awaiting independent calls one after another. Independent work runs in parallel with Promise.all — or Promise.allSettled when one failure shouldn't discard the other results; dependent work stays sequential.
The failure modes to design out are unhandled rejections and forgotten awaits. Every promise is awaited or deliberately handled, and loops over large datasets process in batches rather than firing thousands of concurrent requests at a database or provider.
// Sequential by accident — total time is the sum of all three calls
async function loadDashboardSlow(id) {
const user = await getUser(id);
const plan = await getPlan(id);
const usage = await getUsage(id);
return { user, plan, usage };
}
// Independent calls in parallel — total time is roughly the slowest call
async function loadDashboard(id) {
const [user, plan, usage] = await Promise.all([
getUser(id),
getPlan(id),
getUsage(id),
]);
return { user, plan, usage };
} Express is deliberately minimal, so structure is your responsibility. Middleware handles cross-cutting concerns — authentication, request IDs, rate limiting, body parsing — in a predictable order; routers group endpoints by resource; controllers stay thin and call services; and one error middleware at the end turns thrown errors into consistent responses.
Async route handlers must forward rejected promises to that error middleware. Otherwise a failed request simply hangs until the client times out.
Every body, query string and route parameter is validated against a schema at the edge, before it reaches business logic, and the validated result is what the rest of the code uses. Validation errors return field-level details in the same format everywhere.
Sanitization depends on where data goes: escaping for HTML, parameterized queries for SQL, argument arrays for shell commands — not one generic “clean” function applied on the way in.
Session cookies suit first-party web apps on the same domain; short-lived tokens suit APIs used by other clients and services. Either way, authentication only answers “who is this?”. Authorization is a separate, explicit step that checks role and permission and, in multi-tenant systems, that the requested resource belongs to the caller's tenant.
Third-party APIs fail in every possible way: slow responses, rate limits, partial outages, changed payloads. Each integration gets a client with timeouts, retries with backoff for safe operations, and mapping from the provider's data model to ours, so their quirks don't leak into business logic.
Webhook handlers should assume events can be delayed, retried or delivered more than once. They verify the signature, acknowledge quickly and process asynchronously — idempotently wherever duplicate execution could cause side effects.
Work that's slow, external or retryable doesn't belong in the request cycle. The API validates the request, enqueues a job and returns; a worker picks it up, calls the external service and records the result. Workers run as separate processes, so a backlog can't slow user-facing requests.
Jobs are idempotent, retries use backoff, messages that keep failing land somewhere visible after a retry limit, and payloads carry identifiers and tenant context rather than large objects.
Request
validated at the edge
API
enqueues, returns quickly
Async job
separate worker, retries
External service
timeout, backoff
Result
stored, user notified
AWS Lambda suits Node.js well for event-driven and spiky workloads, but it changes the rules. Functions are short-lived and stateless: anything that must persist goes to a database, cache or storage, and in-memory state may or may not survive between invocations.
Cold starts grow with bundle size and initialization work, so dependencies stay lean and expensive clients are created once, outside the handler. Timeouts are set deliberately below upstream timeouts, concurrency limits protect downstream systems, and retries from asynchronous event sources mean handlers must be idempotent.
A Lambda function that opens a new database connection on every invocation can exhaust a relational database under load. Connection reuse or a connection proxy matters more than the function code.
The data model chooses the database, not the language. Relational databases such as PostgreSQL and MySQL fit transactional business data with relationships and reporting needs. MongoDB fits documents read and written as a unit with a flexible structure. DynamoDB fits well-known access patterns at scale.
Whatever the store, queries are parameterized, connection pools are sized for the deployment model — a pool per Lambda instance behaves very differently from a pool per server — and migrations are versioned with the code.
Production debugging depends on what was logged before the incident. I log structured JSON with a correlation ID that follows a request through the API, queues and workers, plus tenant and user identifiers where appropriate — never secrets, and never full payloads containing personal data. Errors are logged once, with context, at the boundary that handles them.
Secrets come from the environment or a secret manager, never the repository. Inputs are validated; output to HTML, SQL and shell is encoded for its context. Dependencies are a real attack surface in the Node ecosystem, so the dependency list stays short, lockfiles are committed and audit warnings are reviewed rather than ignored.
Login, password reset and expensive endpoints get rate limits, and outbound calls go to allow-listed hosts rather than URLs taken from user input.
Node handles thousands of concurrent I/O operations on one thread because it never waits on them — and it stops handling anything while CPU-bound code runs. Image processing, large data transformations, encryption loops and report generation belong in worker threads, separate services or background jobs.
Large files and exports are streamed rather than loaded into memory, and memory that grows over time is treated as a bug to investigate, not something a restart hides.
In practice
Real products
Node.js has been part of SaaS platforms and integration-heavy systems I've led or built.
Accessibility Technology / SaaS
Accessibility SaaS with a Node.js backend behind a React frontend, integrating Stripe and AWS Bedrock — where I lead delivery and technical decisions.
Full-stack development
Web applications, integrations and internal business systems built with Node.js and Express.js.
Workload-first cloud architecture: serverless, storage, data, security and cost.
Predictable API contracts: validation, errors, authorization, idempotency and webhooks.
Tenancy, permissions, plans, billing and the realities of running SaaS in production.
Applied LLM engineering: structured output, validation, cost and security.
Build reliable web platforms, SaaS products, dashboards, APIs, and internal tools with modern full-stack technologies.
Learn moreDeploy, optimize, and maintain reliable cloud systems using AWS services and practical cloud architecture.
Learn moreDesign and build SaaS products from MVP to production, including architecture, user management, subscriptions, integrations and cloud infrastructure.
Learn moreWhether it's an architecture decision, a delivery problem or something specific to Node.js, I can help you find a practical path forward.
What happens next
Share your goal
A few lines about the product, problem, or audit need.
Get a quick read
A practical reply on the best technical path forward.
Move forward
A clear scope, estimate, or concrete next step.