index.ts

import express, { Express } from "express";
import bodyParser from 'body-parser';
import { listItems, addItem, completeItem } from './routes';


// Configure and start the HTTP server.
const port: number = 8088;
const app: Express = express();
app.use(bodyParser.json());
app.get("/api/list", listItems);
app.post("/api/add", addItem);
app.post("/api/complete", completeItem);
app.listen(port, () => console.log(`Server listening on ${port}`));

routes.ts

import { Request, Response } from "express";
import { ParamsDictionary } from "express-serve-static-core";


// Require type checking of request body.
type SafeRequest = Request<ParamsDictionary, {}, Record<string, unknown>>;
type SafeResponse = Response;  // only writing, so no need to check


// Description of an individual item in the list.
type Item = {
  name: string,
  completedAt: number  // < 0 if not completed
};


// Map from item name to item. (Note that item names must be unique.)
const items: Map<string, Item> = new Map();


/**
 * Returns a list of all items that are uncompleted or were completed less than
 * 5 seconds ago.
 * @param _req the request
 * @param res the response
 */
export const listItems = (_req: SafeRequest, res: SafeResponse): void => {
  const now = Date.now();
  const remaining: {name: string, completed: boolean}[] = [];
  for (const item of items.values()) {
    if (item.completedAt < 0) {
      remaining.push({name: item.name, completed: false});
    } else if (now - item.completedAt <= 5000) {
      remaining.push({name: item.name, completed: true});
    } else {
      // skip this because it was completed too long ago
    }
  }
  res.send({items: remaining});
}


/**
 * Add the item to the list.
 * @param req the request
 * @param res the response
 */
export const addItem = (req: SafeRequest, res: SafeResponse):void => {
  const name = req.body.name;
  if (typeof name !== 'string') {
    res.status(400).send(`name is not a string: ${name}`);
    return;
  }

  // If the item does not already exist, then add it. Notify the client of
  // whether we had to add the item.
  const item = items.get(name);
  if (item === undefined) {
    items.set(name, {name, completedAt: -1});
    res.send({name, added: true});
  } else if (item.completedAt >= 0) {
    item.completedAt = -1;  // no longer removed
    res.send({name, added: true});
  } else {
    res.send({name, added: false});
  }
}


/**
 * Marks the given item as completed.
 * @param req the request
 * @param res the response
 */
export const completeItem = (req: SafeRequest, res: SafeResponse): void => {
  const name = req.body.name;
  if (typeof name !== 'string') {
    res.status(400).send("name is not a string: ${name}");
    return;
  }

  const item = items.get(name);
  if (item === undefined) {
    res.status(400).send(`no item called "${name}"`);
    return;
  } else if (item.completedAt >= 0) {
    res.status(400).send(`item called "${name}" is already completed`);
    return;
  }

  item.completedAt = Date.now();
  res.send({completed: true});
}

routes_test.ts

import * as assert from 'assert';
import * as httpMocks from 'node-mocks-http';
import { listItems, addItem, completeItem } from './routes';

describe('routes', function() {

  it('end-to-end', function() {
    const req1 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
    const res1 = httpMocks.createResponse();
    listItems(req1, res1);
    assert.strictEqual(res1._getStatusCode(), 200);
    assert.deepStrictEqual(res1._getData(), {items: []});

    const req2 = httpMocks.createRequest({method: 'POST', url: '/api/add',
        body: {name: "laundry"}});
    const res2 = httpMocks.createResponse();
    addItem(req2, res2);
    assert.strictEqual(res2._getStatusCode(), 200);
    assert.deepStrictEqual(res2._getData(), {added: true, name: "laundry"});

    // Try to create it again.
    const req3 = httpMocks.createRequest({method: 'POST', url: '/api/add',
        body: {name: "laundry"}});
    const res3 = httpMocks.createResponse();
    addItem(req3, res3);
    assert.strictEqual(res3._getStatusCode(), 200);
    assert.deepStrictEqual(res3._getData(), {added: false, name: "laundry"});

    const req4 = httpMocks.createRequest({method: 'POST', url: '/api/add',
        body: {name: "wash dog"}});
    const res4 = httpMocks.createResponse();
    addItem(req4, res4);
    assert.strictEqual(res4._getStatusCode(), 200);
    assert.deepStrictEqual(res4._getData(), {added: true, name: "wash dog"});

    const req5 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
    const res5 = httpMocks.createResponse();
    listItems(req5, res5);
    assert.strictEqual(res5._getStatusCode(), 200);
    assert.deepStrictEqual(res5._getData(), {items: [
      {name: "laundry", completed: false},
      {name: "wash dog", completed: false},
    ]});

    const req6 = httpMocks.createRequest({method: 'POST', url: '/api/complete',
        body: {name: "wash dog"}});
    const res6 = httpMocks.createResponse();
    completeItem(req6, res6);
    assert.strictEqual(res6._getStatusCode(), 200);
    assert.deepStrictEqual(res6._getData(), {completed: true});

    const req7 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
    const res7 = httpMocks.createResponse();
    listItems(req7, res7);
    assert.strictEqual(res7._getStatusCode(), 200);
    assert.deepStrictEqual(res7._getData(), {items: [
      {name: "laundry", completed: false},
      {name: "wash dog", completed: true},
    ]});
  });

});

Full Code