Practice Lab Guide

Project 11: Typed IoT Backend API

Build a strongly-typed REST API in Express using TypeScript interfaces to process incoming sensor telemetry.
Domain
Backend Engineering / TS Types
Difficulty
⭐⭐⭐☆☆ (Intermediate)
Course Module
TypeScript for IoT
Deliverables
TS Router Files, Request validation Middlewares
1. Express Middleware Validation Lifecycle

Accepting raw payloads from remote devices without validating fields can lead to database corruption or server crashes. The diagram below details the Express middleware validation lifecycle. Incoming HTTP POST requests flow sequentially through logging and validation middlewares before reaching the route handlers. The schema validator verifies types (e.g. confirming `temperature` is a numeric float), rejecting malformed packets with a 400 Bad Request response.

HTTP POST Request /api/telemetry {"temp": "twenty"} Logger Middleware Logs method & IP Calls next() Validation Middleware Is temp numeric? No! -> Fail 400 Bad Request Interrupts process Route Handler Saves to DB 201 Created
2. Part 1: Step-by-Step Individual Code Implementation & Testing Steps

Follow these detailed steps to build the routing directory structure, write the schemas, configure validation middleware, and execute curl checks.

STEP 1

Launch VM Terminal and Create API Workspace

Boot up your VirtualBox Ubuntu machine. Open the terminal (Ctrl+Alt+T) and navigate to the project directory.

ubuntu@iot-vm:~$ cd ~/workspace/backend_scaffold && mkdir -p src/routes src/middlewares src/interfaces
We run `cd` and `mkdir` to generate a modular folder structure (`routes`, `middlewares`, `interfaces`) inside our backend workspace.
STEP 2

Create TypeScript Interface Specs File

Open a new file in gedit to write the TypeScript type definitions for the API.

ubuntu@iot-vm:~/workspace/backend_scaffold$ gedit src/interfaces/telemetry.ts &
We open `telemetry.ts` inside the interfaces folder to define type constraints for telemetry payloads and device profiles.
STEP 3

Create Request Validation Middleware File

Open a new file in gedit to write the request validation middleware.

ubuntu@iot-vm:~/workspace/backend_scaffold$ gedit src/middlewares/validate.ts &
We open `validate.ts` inside the middlewares folder to write functions that validate request bodies against our TS interfaces.
STEP 4

Create Router Module File

Open a new file in gedit to define the routing endpoints for the API.

ubuntu@iot-vm:~/workspace/backend_scaffold$ gedit src/routes/api.ts &
We open `api.ts` inside the routes folder to define route handlers for fetching device lists, publishing telemetry, and routing commands.
STEP 5

Integrate Router and Middleware in Server Entry File

Update the main `src/index.ts` file to register the validation middleware and API router.

ubuntu@iot-vm:~/workspace/backend_scaffold$ gedit src/index.ts &
We open `src/index.ts` to register the middleware pipelines and router paths, completing the API configuration.
STEP 6

Compile and Run the Server

Compile the TypeScript code and start the server using npm.

ubuntu@iot-vm:~/workspace/backend_scaffold$ npx tsc && node dist/index.js
We compile the TypeScript code to JavaScript using `tsc` and start the server to test the endpoints.
STEP 7

Test Validation Middleware with Malformed Request

Send an invalid payload (e.g. with a string instead of a number) using curl to verify that the server rejects the request with a 400 response.

ubuntu@iot-vm:~$ curl -i -X POST http://localhost:3000/api/telemetry \ -H "Content-Type: application/json" \ -d '{"deviceId":"node_1", "temperature":"warm"}'
We send a malformed request to verify that the validation middleware catches the type mismatch and rejects the request with a 400 Bad Request error.
STEP 8

Verify API with a Valid Telemetry Request

Send a valid payload (with a numeric temperature value) using curl to verify that the server processes the request and returns a 201 response.

ubuntu@iot-vm:~$ curl -i -X POST http://localhost:3000/api/telemetry \ -H "Content-Type: application/json" \ -d '{"deviceId":"node_1", "temperature":24.5, "light":450}'
We send a valid telemetry request to verify that the middleware passes the check and the route handler processes the data, returning a 201 Created response.
3. Router Endpoint Matrix Map

The table below lists the available API routes, including their HTTP methods, payload structures, validation requirements, and expected responses.

HTTP Method & Endpoint Request Payload Schema Validation Rules Response Codes & Values
GET /api/devices None No body check 200 OK: Array of device profiles
POST /api/telemetry { deviceId, temperature, light } deviceId (str), temp (float), light (int) 201 Created / 400 Bad Request
POST /api/commands { deviceId, command } command must match "ON" or "OFF" 200 OK / 400 Bad Request
4. Part 2: Complete Codebases & Router Implementations

Below is the complete C++ firmware code showing how abstract inheritance interfaces work, followed by the complete Python script to listen to the serial port and print the data.

Asset 1: TypeScript Interfaces File (`src/interfaces/telemetry.ts`)

// Interface definitions for IoT entities export interface Device { id: string; name: string; location: string; status: 'active' | 'inactive'; } export interface TelemetryReading { deviceId: string; temperature: number; light: number; timestamp?: string; } export interface ActuatorCommand { deviceId: string; command: 'ON' | 'OFF'; }

Asset 2: Validation Middleware File (`src/middlewares/validate.ts`)

// Request validation middleware using Type Guards import { Request, Response, NextFunction } from 'express'; export const validateTelemetry = (req: Request, res: Response, next: NextFunction) => { const { deviceId, temperature, light } = req.body; if (typeof deviceId !== 'string' || deviceId.trim() === '') { return res.status(400).json({ error: 'Invalid field value: deviceId must be a non-empty string' }); } if (typeof temperature !== 'number' || isNaN(temperature)) { return res.status(400).json({ error: 'Invalid field value: temperature must be a valid number' }); } if (typeof light !== 'number' || !Number.isInteger(light)) { return res.status(400).json({ error: 'Invalid field value: light level must be a valid integer' }); } return next(); // Validation passed, call next handler }; export const validateCommand = (req: Request, res: Response, next: NextFunction) => { const { deviceId, command } = req.body; if (typeof deviceId !== 'string' || deviceId.trim() === '') { return res.status(400).json({ error: 'Invalid field value: deviceId must be a non-empty string' }); } if (command !== 'ON' && command !== 'OFF') { return res.status(400).json({ error: 'Invalid command: value must be ON or OFF' }); } return next(); };

Asset 3: Express Router Module File (`src/routes/api.ts`)

import { Router, Request, Response } from 'express'; import { validateTelemetry, validateCommand } from '../middlewares/validate'; import { Device, TelemetryReading } from '../interfaces/telemetry'; const router = Router(); // In-memory database store const devices: Device[] = [ { id: 'node_1', name: 'ESP32_Office', location: 'Room 101', status: 'active' } ]; const telemetryHistory: TelemetryReading[] = []; // GET: Retrieve device list router.get('/devices', (req: Request, res: Response) => { res.status(200).json(devices); }); // POST: Submit telemetry readings router.post('/telemetry', validateTelemetry, (req: Request, res: Response) => { const { deviceId, temperature, light } = req.body; const reading: TelemetryReading = { deviceId, temperature, light, timestamp: new Date().toISOString() }; telemetryHistory.push(reading); console.log(f"[DB LOG] Saved telemetry from {deviceId}. Temp: {temperature} C"); res.status(201).json({ message: 'Telemetry saved successfully', reading }); }); // POST: Route commands to actuators router.post('/commands', validateCommand, (req: Request, res: Response) => { const { deviceId, command } = req.body; console.log(f"[CMD OUT] Command {command} successfully routed to {deviceId}."); res.status(200).json({ message: f"Command {command} sent to {deviceId}" }); }); export default router;

Asset 4: Updated Server Entry File (`src/index.ts`)

import express, { Request, Response } from 'express'; import apiRouter from './routes/api'; const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); // Custom Logger Middleware app.use((req: Request, res: Response, next) => { console.log(f"[HTTP] {req.method} {req.url} from {req.ip}"); next(); }); // Register API Router app.use('/api', apiRouter); // Health Check app.get('/health', (req: Request, res: Response) => { res.status(200).json({ status: 'online', timestamp: new Date().toISOString() }); }); app.listen(PORT, () => { console.log(f"TypeScript API Server listening on port {PORT}"); });
5. Deliverables Summary

Created Workspace Assets

  • TypeScript schema interface: src/interfaces/telemetry.ts.
  • Validation middleware module: src/middlewares/validate.ts.
  • Express router: src/routes/api.ts.

Verification Proof

  • TypeScript compilation logs showing zero errors.
  • Screenshot of the terminal running curl validation checks, displaying the 400 Bad Request response for invalid data and the 201 Created response for valid payloads.
6. Closing Explanation: Why We Did This & What It Accomplishes

Architectural Intent & Operational Impact

Why We Did This

What This Accomplishes