Day 1: Bootstrap a Transaction API — and Wire Up Its First Endpoint

Lesson 1 60 min

Day 1: Bootstrap a Transaction API — and Wire Up Its First Endpoint

Welcome to the first day of building a hardened Node.js backend service! In this course, we're not just learning to code; we're learning to build systems that survive the harsh realities of production. Today, we're laying the absolute foundation: getting our service to speak to the outside world through its first API endpoint.

The 3 AM Incident: Why API Contracts Matter

You've just been paged at 3 AM. A critical client integration is failing. They're sending requests to your shiny new transaction API, but nothing works. Logs are full of generic server errors, and the client reports "bad request." The problem isn't a server crash; it's a misunderstanding. The client isn't sending what your server expects, and your server isn't telling the client why. This isn't a code bug, it's a contract bug.

Poorly defined or enforced API contracts are a silent killer. They lead to integration friction, wasted debugging cycles, and cascading failures as clients retry invalid requests, hammering your service unnecessarily. Think about the early days of APIs like PayPal's or Twitter's. Initial designs, while functional, often lacked the clarity and strictness needed for broad adoption, leading to complex documentation and significant developer support overhead. Today, we establish a crystal-clear contract from the very first line of code.

The Problem: A Silent Service

Right now, your service is a blank slate. It exists, conceptually, but it can't accept a transaction request, validate it, or return a response. Clients have no way to interact with it. Our goal for today is to build a POST /transactions endpoint that acts as the entry point for all future transaction requests.

The Vending Machine Contract: Intuition for APIs

Imagine an old-school vending machine. It has clear slots for coins, labeled buttons for snacks, and a dispenser for your item. If you put in a crumpled dollar bill, it spits it back out. If you press a button for an empty slot, it tells you "OUT OF STOCK." It never just silently eats your money or crashes.

Your API endpoint is that vending machine. It defines:

  1. Inputs: What kind of data it expects (e.g., amount as a number, currency as a string).

  2. Actions: What it does with that data (e.g., "process transaction").

  3. Outputs: What it returns (e.g., a success message with a transaction ID, or a clear error if the input was wrong).

This explicit "contract" is what we're building today.

Component Architecture: Our First Node.js Service

Component Architecture

Client App Node.js Express Transaction API HTTP POST /transactions HTTP 200/400 Response API Boundary: Strict Contract Enforcement

For Day 1, our architecture is beautifully simple. We'll have a single Node.js Express application. This application will listen for incoming HTTP requests, specifically POST requests to the /transactions path.

Component Architecture: A client sends an HTTP POST request directly to our Node.js Express application, which serves as the sole API component for now. This boundary is where our API contract is enforced.

The Request Flow: From Client to Response

Flowchart

Client POST Express App Route Handler Validate Input? Generate Temp ID Respond 200 OK Respond 400 Bad Request Client Response Yes No Early Validation Prevents Waste

When a client sends a POST /transactions request with a JSON body, here's how our service will handle it:

  1. Request Reception: The Express application receives the HTTP request.

  2. Route Matching: Express identifies that the request matches our /transactions route.

  3. Input Parsing: The request body (JSON) is parsed into a JavaScript object.

  4. Input Validation: We check if the parsed input conforms to our contract (e.g., amount is a number, currency is a string).

  5. Response Generation:

  • If valid: We generate a temporary transaction ID and send a 200 OK response.

  • If invalid: We send a 400 Bad Request response with a clear error message.

Request Flow : This flowchart illustrates the journey of a transaction request from the client through validation and eventual response, highlighting the decision point for valid vs. invalid input.

Implementing the Endpoint: POST /transactions

Let's get into the core of our service. We'll use Express to set up a server, parse JSON bodies, and define our first route.

First, we need a basic Express setup in src/server.js:

javascript
// src/server.js
const express = require('express');
const app = express();
const port = 3000;

// Middleware to parse JSON request bodies
app.use(express.json());

// Our first endpoint: POST /transactions
app.post('/transactions', (req, res) => {
  const { amount, currency, description } = req.body;

  // Basic input validation
  if (typeof amount !== 'number' || amount <= 0) {
    return res.status(400).json({ error: 'Invalid amount. Must be a positive number.' });
  }
  if (typeof currency !== 'string' || currency.length !== 3) {
    return res.status(400).json({ error: 'Invalid currency. Must be a 3-letter string (e.g., USD).' });
  }
  if (typeof description !== 'string' || description.length < 5) {
    return res.status(400).json({ error: 'Invalid description. Must be at least 5 characters.' });
  }

  // For now, we just acknowledge receipt and generate a temporary ID
  const transactionId = `temp-tx-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
  console.log(`Received valid transaction: ${JSON.stringify(req.body)} -> ${transactionId}`);

  res.status(200).json({
    id: transactionId,
    message: 'Transaction request received successfully (not yet persisted).',
    status: 'PENDING'
  });
});

app.listen(port, () => {
  console.log(`Transaction API listening at http://localhost:${port}`);
});

Here, app.use(express.json()) is crucial. It tells Express to automatically parse incoming request bodies with a Content-Type: application/json header into a JavaScript object available on req.body. Without this, req.body would be undefined.

Our app.post handler then performs basic validation. Notice how we return res.status(400).json(...) immediately for invalid input. This is critical: fail fast and explicitly. Don't try to guess client intent; enforce the contract. If the input is valid, we generate a temporary ID and respond with a 200 OK. This temporary ID is a placeholder, demonstrating that the request was understood and accepted by the API.

API Interaction States

State Machine

API Interaction State Machine POST /transactions — Contract Lifecycle POST Request Invalid Contract (400) Valid Input Pass Send Payload IDLE Awaiting Request VALIDATING Checking Schema REJECTED 400 Bad Request ACCEPTED Temp ID Generated 200 OK Response Sent

To better understand our contract, consider the states of an interaction:

API Interaction States: This diagram illustrates the client's journey through the API, from initiating a request to receiving a definitive response based on input validity.

The Failure Demo: Breaking the Contract

This is where the rubber meets the road. We'll deliberately send an invalid request to our API.

Before today: Sending malformed data might have crashed the server, or resulted in an opaque 500 Internal Server Error, leaving the client guessing.
After today: Our server will explicitly reject the request with a 400 Bad Request and a clear, structured error message. This is a feature, not a bug.

Try sending a transaction with an invalid amount:

bash
curl -X POST -H "Content-Type: application/json" -d '{"amount": "not_a_number", "currency": "USD", "description": "Failed test"}' http://localhost:3000/transactions

You should see an HTTP 400 response with a clear error message like {"error": "Invalid amount. Must be a positive number."}. This command proves our validation works.

Now, a valid request:

bash
curl -X POST -H "Content-Type: application/json" -d '{"amount": 100.50, "currency": "EUR", "description": "Groceries purchase"}' http://localhost:3000/transactions

This should yield a 200 OK response with a temporary transaction ID.

Scaling Up: From Laptop to Hyperscale

On your laptop, a few if statements are fine. In a hyperscale environment receiving hundreds of millions of requests per second, this basic validation pattern scales by:

  • Dedicated Middleware: Moving validation logic into separate middleware functions or even dedicated microservices (e.g., a "validation service" for complex rules).

  • Schema Validation Libraries: Using robust libraries like Joi or Yup to define complex schemas that automatically validate input and generate detailed error messages. This prevents boilerplate if statements.

  • API Gateways: Often, initial validation (e.g., for required headers, basic JSON structure) occurs even before the request hits your service, at an API Gateway layer (like AWS API Gateway, Google Cloud Endpoints, or an open-source solution like Kong). This offloads work from your application servers.

The core principle remains: enforce the contract as early and explicitly as possible. This reduces load, prevents bad data from propagating, and significantly improves debuggability. The alternative — letting invalid data through — leads to cascading failures, data corruption, and the dreaded 3 AM page.

What's Next: Persistence

Today, our transaction requests are received, validated, and acknowledged, but they vanish into the ether after the response is sent. There's no persistence. In the next lesson, Day 2: Persist Transaction Data to Postgres — with Idempotent Writes, we'll connect our API to a real database (Postgres) and ensure that every valid transaction request is durably stored, building towards a truly functional service.


Assignment: Enhance Validation

Your mission, should you choose to accept it, is to add two more validation rules to our POST /transactions endpoint:

  1. Currency Code Check: Ensure the currency field is one of a predefined set of valid ISO 4217 currency codes (e.g., "USD", "EUR", "GBP", "JPY"). If it's not, return a 400 Bad Request with an appropriate error.

  2. Amount Precision: Ensure the amount field, if a decimal, has no more than two decimal places (e.g., 100.25 is valid, 100.256 is invalid). This is crucial for financial transactions. Hint: You can convert the number to a string and check its decimal part.

Success Criteria:

  • A curl request with an invalid currency code (e.g., "XYZ") returns a 400 Bad Request.

  • A curl request with an amount like 100.123 returns a 400 Bad Request.

  • A curl request with a valid currency and amount (e.g., 100.50, "USD") returns a 200 OK.


Solution Hints

For the currency check, you could define an array of valid currencies: const VALID_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY']; and then use VALID_CURRENCIES.includes(currency) in your if statement.

For amount precision, you can convert the amount to a string, find the decimal point, and check the length of the fractional part:

javascript
const amountString = amount.toString();
const decimalIndex = amountString.indexOf('.');
if (decimalIndex !== -1 && (amountString.length - 1 - decimalIndex) > 2) {
  // Amount has more than two decimal places
  return res.status(400).json({ error: 'Amount has too many decimal places.' });
}

Questions & Discussion

Leave a Reply

Your email address will not be published. Required fields are marked *