4 min read
I made this simple project to learn and to provide example of how to make simple API in Node.js with Express.js middleware (and Typescript but what did you expect 😅).
This project was made to be simple example, how to implement API with Express and to have a simple to understand documentation for every one to use and learn.
Link to GitHub repo here.
If you find a bug or want to suggest a new feature, feel free to create an issue on GitHub page
I use this by myself to check if DDNS I use is correctly running and pointing to my homelab. The service is running on docker, managed by Portainer and behind Nginx Proxy Manager. I check if the service is up with Uptime-Kuma, this way I can see if all parts of the chain are working correctly.
http://address:port/healthcheck or http://address:port/{"Status" : "OK"} or plaintext OK404 statusVisit GitHub page
Below are just short code snippets, check the GitHub for full source files.
Overview of the main source files. If you see ... then I left the code out because is not important or repeating.
In the index.ts we load the configuration and start the Express HTTP server. After that, we let know the user what configuration we are running and if it’s incorrect we terminate the application.
dotenv.config(); // initialize configuration for .env file
// start HTTP server to be listeningapp.listen(port, host, () => { log.info(`⚡️ : Server is running at http://${host}:${port}`); checkEnvEndpoints(); // show what setting we are using and check if they are correct});In app.ts we initialize the Express.js HTTP server, we set it up to use JSON middleware and our routes defined in routes.ts. Other than, we load the server settings from environment variables.
// loading the config variables for server and routes configexport const port = Number(process.env.PORT ?? 8082);...export let rootRes = Boolean(process.env.ROOT_RES ?? true);
const app = express(); // init expressapp.use(express.json()); // init to use json middlewareroutes(app); // load our routes (defined in routes.ts)
export default app; // export app express object (used mostly in index.ts)
// helper function to setup env variables outside of app.ts (used for testing)export function setEnv(JSON: boolean, endpoint: boolean, root: boolean) {...}In routes.ts we register and define behavior of our routes.
// default function to handle route registrationexport default function (app: Express) { // define a route handler for the root route app.get('/', (req: Request, res: Response) => processRoot(req, res));
//define a route for the /healthcheck route app.get('/healthcheck', (req: Request, res: Response) => processEndpoint(req, res));}
// handle root routefunction processRoot(req: Request, res: Response) { if (rootRes) res.send(processMessage(req, res)); // if enabled respond else res.sendStatus(404); // if not return 404}
// handle /healthcheck routefunction processEndpoint(req: Request, res: Response) {...}
// log request (if testing) and response with JSON or plaintext based on settingsfunction processMessage(req: Request, res: Response) { if (process.env.NODE_ENV != 'test') log.info(`⚡️ : Request from: ${req.ip}`);
if (useJSON) { res.type('json'); // set response type return '{"status": "OK"}'; // return response } else {...}}I am using Jest for testing. No real preference here, more like I found it after quick search and was pretty easy to use. Sometimes I use Postman for API testing and development, it has really nice interface and it is configurable, but it is completely separated from your codebase what may make you duplicate a some code.
jest.config.js /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */module.exports = { preset: 'ts-jest', testEnvironment: 'node'};__tests__ and file app.test.ts // setup before all tests beginbeforeAll(() => { process.env.NODE_ENV = 'test'; // set env var to test to enable logs});
// setup before each testbeforeEach(() => { server = app.listen(port, host); // start express server});
// after each test is doneafterEach(() => { server.close(); // stop the express server});
// testing endpoint /healthcheckdescribe('ENDPOINT: /healthcheck', () => { // test JSON response it('JSON - 200', async () => { // set env var for the test // enable JSON and healthcheck endpoint disable root endpoint setEnv(true, true, false);
const res = await request(app).get('/healthcheck'); // run request
expect(res.statusCode).toBe(200); // check for 200 expect(res.body).toEqual({ status: 'OK' }); // check for JSON response });
// test plaintext response it('TEXT - 200', async () => { // set env var for the test // enable plaintext and healthcheck endpoint disable root endpoint setEnv(false, true, false);
const res = await request(app).get('/healthcheck'); // run request
expect(res.statusCode).toBe(200); // check for 200 expect(res.text).toEqual('OK'); // check for plaintext response });
// test if we contact root when disabled we get 404 it('/ - 404', async () => { // set env var for the test // enable plaintext and healthcheck endpoint disable root endpoint setEnv(false, true, false);
const res = await request(app).get('/'); // run request
expect(res.statusCode).toBe(404); // check for 404 });});npm run test and output will look like somthing this PASS __tests__/app.tests.ts ENDPOINT: /healthcheck ✓ JSON - 200 (14 ms) ✓ TEXT - 200 (2 ms) ✓ / - 404 (1 ms) ENDPOINT: / ✓ JSON - 200 (2 ms) ✓ TEXT - 200 (1 ms) ✓ /healthcheck - 404 (2 ms) ENDPOINT: both ✓ / - 404 (1 ms) ✓ /healthcheck - 404 (1 ms)Test Suites: 1 passed, 1 totalTests: 8 passed, 8 totalSnapshots: 0 totalTime: 0.977 sRan all test suites.We use GitHub docker repository to distribute the project. Also docker is easy way to run it on server.
To make a docker container we need to make a dockerfile. And build it. This dockerfile has two stages.
# stage one# we base you dockerfile on alpine linux with node preinstalledFROM node:16.15.1-alpine AS node-build
# set our work directoryWORKDIR /usr
# copy package.json tsconfig.json and all the source filesCOPY package.json ./COPY tsconfig.json ./COPY src ./src
# run npm to install packages and build the projectRUN ls -aRUN npm installRUN npm run build
# stage two# again we base the package on alpine linux with nodeFROM node:16.15.1-alpineWORKDIR /usr
# set environment variable that we are in productionENV NODE_ENV production
# now we have builded the project so we copy only package.json and install themCOPY package.json ./RUN npm install
# we copy the builded source files only and install pm2 package globally# pm2 (production manager) will let out app run indefinitely (and som other stuff)COPY --from=0 /usr/build/src .RUN npm install pm2 --location=global
# start the applicationCMD ["pm2-runtime","index.js"]docker build . (you have to be in healthcheck folder)Successfully built df158448c3a7docker run -p 8082:8082 df158448c3a7 argument -p is to publish a port so we can it access the API from outside of the containerlocalhost:8082/ in your browser or you can use Postman/InsomniaIf you want to deploy this project on your server the easiest way is to use docker-compose. It’s nicely reusable and readable.
version: '3.0'services: healthcheck: # name of the service image: ghcr.io/matus-barta/healthcheck:latest # addres to the image in the repository container_name: healthcheck # name of the container environment: # here we setup our env vars - PORT=8082 # listening port - HOST="localhost" # listening IP address - ROOT_RES="true" # https://<domain>/ - ENDPOINT_RES="false" # https://<domain>/healthcheck - JSON_RES="true" # response in JSON ports: - 8082:8082 # ports we will be listening on (external port : internal port) restart: unless-stopped # dont stop the service security_opt: - no-new-privileges:true # some security stuff not really neededI was thinking to add part about GitHub Actions but honestly I don’t understand it enough to run you thru it what it does. I based it on Techno Tim’s project LittleLink. Also good GitHub Actions explanation by Fireship on YT.
To start, you will need machine running docker service. See how to install docker on Ubuntu.
To automatically check the Healthcheck is online, I use Uptime-Kuma with notifications sent to Telegram (or you can use Discord and many other messaging platforms)
Here are my Uptime-Kuma setting for the Healthcheck 
Hope you find this simple project useful for your homelab or as a way to learn how to make you own simple API. Healthcheck is not something made for real production or as example “how it should be done”. No I am just development and homelab enthusiast so, if this has at least some usefulness for you, I will be super extremely happy. Enjoy your day!