Simple TypeScript + Node Template for Interviews - async main, web api, and tests
Date: 2025-03-26 | craft | create | node | typescript |
I've been interviewing for remote software engineer roles for the past several weeks and one interview type has proven more clunky than others - the "real world" interview. Typically the company wants you to build a program in your own environment to see how you use real world tools to interact with things like files and APIs.
On the surface this makes sense but if you're not prepared for it you can easily lose a good ~15 minutes of your 45 minute interview just trying to get your basic project setup (when's the last time you built a project from nothing?). I got burned by this a few times as I fiddled with getting typescript transpilation working or setting up an Express server so I could build endpoints so decided to do something about it.
In this post we'll walk through a template I built to make it easy to come into a "real world" interview ready to focus on the problem, not fiddle with my setup.
HAMINIONs members get access to all the project files (github) so you can git clone and start running it as well as dozens of other example projects we talk about here on the blog. (You also support me making more posts like this - thank you!).
Template Overview
In most of these "real world" interviews you get ~45 minutes to get a working solution for whatever the problem is. They often fall into one or more of these buckets:
- Build a web api that does x - Might be CRUD or building rate limiters
- Integrate with an existing API to do x- Like getting train schedules from a public API or analyzing mock data you get back from a custom API
- Read files, do some operations, then write to some files - Like analyzing customer data stored in JSON / CSV and then saving the outputs
The problems themselves are usually about LeetCode easy - medium but the added overhead of piping inputs / outputs from / to various formats can trip you up if you haven't been practicing that.
Therefore the goal of this template is to provide a Simple Scalable System providing common tools to handle this overhead so you can focus on the logic in the small amount of time you have to complete the task:
- Transpiling and running TypeScript
- Async main function (so you can write things async and avoid awkward Promise.resolve / .then chains)
- Express web server for building simple endpoints
- Helper methods for reading / writing files and get / post requests
- Jest tests so you can write / run tests
This covers all the "overhead" cases I've had to face in these kinds of interviews over the past few weeks and is what I bring to these kinds of interviews today.
Note: Not all cos are cool with you bringing your own code but usually those cos will provide their own form of base template to use so you start out in ~roughly the same place.
Running the Boilerplate
First install all dependencies: npm install
Then you can run:
- Test:
npm run test
- Main function / Web server (port 5000):
npm run start
Code Walkthrough
File Layout:
- src
- fetch_utils.ts
- file_utils.ts
- index.ts
- todo_service.ts
- tests
- index.tests.ts
- jest.config.js
- package.json
- tsconfig.json
Package.json - Scripts and dependencies.
{
"name": "202503_node-typescript-interview-boilerplate",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "tsc && node dist/index.js",
"dev": "ts-node src/index.ts",
"test": "jest"
},
"keywords": [],
"author": "hamy labs",
"license": "",
"description": "",
"devDependencies": {
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.2.6",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
},
"dependencies": {
"express": "^4.21.2"
}
}
index.ts - Entrypoint.
- Main function to run your logic directly
- Sets up express server so easy to see how to add / modify endpoints
import { get_user_todo, post_user_todo } from "./todo_service";
import { Express } from "express";
import express = require("express");
export async function main(message: string = "Hello, TypeScript!"): Promise<string> {
console.log(message);
const todo_result = await get_user_todo()
console.log("TodoResult: ", todo_result)
const post_todo_result = await post_user_todo(todo_result)
console.log("PostResult: ", post_todo_result)
return message;
}
// Execute the main function when this file is run directly
if (require.main === module) {
main().catch(error => console.error('Error in main function:', error));
}
const app = express()
const port = 5000
app.get("/", (req, res) => {
res.send("iamanendpoint")
})
app.listen(port, () => {
console.log("Listening on port: ", port)
})
todo_services.ts - An example business logic file showing fetchData / postData
- I usually comment out the code calling this in index.ts at the start of an interview but like to have it so I know my program is running okay at the start.
import { fetchData, postData } from "./fetch_utils"
type Todo = {
userId: number,
id: number,
title: string,
completed: boolean
}
export async function get_user_todo(): Promise<Todo> {
const todo_result = await fetchData<Todo>('https://jsonplaceholder.typicode.com/todos/1')
return todo_result
}
export async function post_user_todo(todo: Todo): Promise<Todo> {
const post_result = await postData<Todo>('https://jsonplaceholder.typicode.com/posts', todo)
return post_result
}
fetch_utils.ts - Simple get and post json data utils.
export async function fetchData<T>(url: string, headers?: HeadersInit): Promise<T> {
try {
// Set up default headers with content type
const defaultHeaders: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...headers // Spread the custom headers to override defaults if needed
};
// Make the fetch request
const response = await fetch(url, {
method: 'GET',
headers: defaultHeaders,
// You can add more fetch options here as needed
});
// Check if the request was successful
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Parse and return the JSON data with type T
const data: T = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error; // Re-throw to allow caller to handle
}
}
export async function postData<T, R = any>(url: string, data: T, headers?: HeadersInit): Promise<R> {
try {
// Set up default headers with content type
const defaultHeaders: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...headers // Spread the custom headers to override defaults if needed
};
// Make the fetch request
const response = await fetch(url, {
method: 'POST',
headers: defaultHeaders,
body: JSON.stringify(data),
// You can add more fetch options here as needed
});
// Check if the request was successful
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Parse and return the JSON data with type R
const responseData: R = await response.json();
return responseData;
} catch (error) {
console.error('Error posting data:', error);
throw error; // Re-throw to allow caller to handle
}
}
file_utils.ts - Simple read / write file utils.
import { promises as fsPromises } from 'fs';
export async function readFileAsync(filePath: string): Promise<string> {
try {
// Read the file asynchronously using promises
const data: string = await fsPromises.readFile(filePath, 'utf8');
return data;
} catch (error: any) {
console.error(`Error reading file asynchronously: ${error.message}`);
throw error;
}
}
export async function writeFileAsync(filePath: string, content: string): Promise<void> {
try {
// Write the file asynchronously using promises
await fsPromises.writeFile(filePath, content, 'utf8');
console.log(`Successfully wrote to ${filePath}`);
} catch (error: any) {
console.error(`Error writing file: ${error.message}`);
throw error;
}
}
tsconfig.json - Configure TypeScript
- I am honestly not an expert in TS transpilation so am sure there's better settings but this worked fine for me.
{
"compilerOptions": {
"target": "ES2018",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}
jest.config.js - Configure Jest
- Similar to above - I'm sure there are better settings
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.ts'],
verbose: true
};
index.test.ts - Tests
import { main } from '../src/index';
import { readFileAsync, writeFileAsync } from "../src/file_utils"
describe('main function', () => {
// Save the original console.log
const originalConsoleLog = console.log;
let consoleOutput: string[] = [];
// Mock console.log before each test
beforeEach(() => {
consoleOutput = [];
console.log = jest.fn((message) => {
consoleOutput.push(message);
});
});
// Restore console.log after each test
afterEach(() => {
console.log = originalConsoleLog;
});
test('should print default message to console', async () => {
const result = await main();
expect(result).toBe('Hello, TypeScript!');
expect(consoleOutput[0]).toBe('Hello, TypeScript!');
});
test('should print custom message to console', async () => {
const customMessage = 'Custom message';
const result = await main(customMessage);
expect(result).toBe(customMessage);
expect(consoleOutput[0]).toBe(customMessage);
});
});
describe("Test file read/write", () => {
type ExampleType = {
a: string
b: number
}
test("Should read/write", async() => {
const file_path = "./resources/temp/example.json"
const file_payload: ExampleType = {a: "iamafile", b: 1}
const write_result = await writeFileAsync(file_path, JSON.stringify(file_payload))
const read_result = await readFileAsync(file_path)
const parsed_result = JSON.parse(read_result) as ExampleType
expect(parsed_result.a).toBe(file_payload.a)
expect(parsed_result.b).toBe(file_payload.b)
})
})
Next
So that's the little TS + Node template I've been using in interviews. It's definitely not production ready but is simple enough to be okay to interviewers and has covered a lot of the common overhead operations so I can focus on the logic itself.
As a reminder, HAMINIONs members get full access to the project files (github) so join if you want to git clone and run the project.
If you liked this post you might also like:
Want more like this?
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.