Security is one of the key concerns for APIs in the current digital landscape and restriction access to your APIs behind an authentication wall is one way to secure your API. There are several authentication techniques that can be used to secure your API with API Key authentication being one such method. In API Key authentication the server looks for a valid API key in the request before it returns a response. For example, in case of a REST API if a client sends a GET request to the server to fetch a resource, the server checks for and would reject the request if it does not have a valid API key. This post provides a walkthrough with code samples on how to build NodeJS API with API key authentication and rate limiting. Read on to find the Github link for the entire source code for this blog post.

Scenario

Say you are building a currency conversion app for which you need to build a REST API to get exchange rate information. The API needs to be secured with API key authentication such that there is an API key generated for the user when creating a user record. The user can use the API key every time the user calls the API. You also need to think about how you can rate limiting to the API to prevent DDoS attacks.

Note: while the code in this post has code to do rate limiting, the rate limiting concept will be discussed in the next post.

NodeJS API with API key authentication

First let’s start with the package.json file

{
  "name": "typescript_api_key_auth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "devStart": "nodemon ./index.ts",
    "start": "node ./dist/index.js",
    "build": "tsc --project ./tsconfig.json",
    "localRun": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^18.15.3",
    "@types/node-localstorage": "^1.3.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "node-localstorage": "^2.2.1",
    "typescript": "^5.0.2"
  },
  "devDependencies": {
    "concurrently": "^7.6.0",
    "nodemon": "^2.0.21"
  }
}

Since a you are working with a Typescript project, it’s a good idea to add the ts.config.json file too.

{
  "compilerOptions": {
    "target": "es2016",                                
    "module": "commonjs",                              
    "outDir": "./dist",                                
    "esModuleInterop": true,                            
    "forceConsistentCasingInFileNames": true,           
    "strict": true
  }
}

index.js

import express, { Express, Request, response, Response } from 'express';
import { add_user, getExchangeRate, getUser } from './src/user_db';

const app: Express = express();
const port = 3000;

app.use(express.json());

app.get('/', (req: Request, res: Response) => {
  res.send('Express + TypeScript Server');
});

app.get('/ping', (req: Request, res: Response) => {
    res.status(200).send({"message": "Hello Concurrently"});
});

app.post("/user", (req: Request, res: Response) => {
    const body = req.body;
    if(!body) {
        res.status(400).send("Error add user request must have a body")
    }
    const new_user = add_user(body.username);
    res.status(201).send(new_user);
});

app.get("/exchangeRates", (req: Request, res: Response) => {
    const apikey = req.headers["x-api-key"] as string;
    if(!apikey) {
        res.status(400).send("Missing API key in header");
    }
    const user = getUser(apikey);
    if(!user) {
        res.status(403).send("Invalid API key");
    }
    //otherwise, increment the usage count and return exchange rates
    const data = getExchangeRate(user!);
    if(typeof data == "string"){
        res.status(429).send(data);
    } else {
        res.status(200).json(data);
    }
});

app.listen(port, () => {
  console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});

Here's the code for userDB.ts

Here’s the code for userDB.ts

import { LocalStorage } from "node-localstorage";
import { dummy_data } from "./dummy_ex_rate";

interface User {
    id: string,
    api_key: string,
    username: string,
    usage?: {"day": string, "count": number}[],
}
const users: User[] = [];
const USER_STR = "USERS";
const localStorage = new LocalStorage("./users");
const MAX_DAILY_API_CALLS = 10;
export const add_user = (username: string): User => {
    const api_key = [...Array(30)]
    .map((e) => ((Math.random() * 36) | 0).toString(36))
    .join('');
    const user: User = {
        id: "USR_"+ new Date().getTime() +"",
        api_key: api_key,
        username: username
    };
    // maintain a local record
    users.push(user);
    // save it to local storage
    localStorage && localStorage.setItem(USER_STR, JSON.stringify(users));
    return user;
};

export const getUser = (api_key: string): User | undefined => {
    return users.find((user) => user.api_key == api_key);
}

export const getExchangeRate = (user: User): any => {
    const today = new Date();
    const todayStr = today.getDate() + "-" + (today.getMonth() +1) + "-" + today.getFullYear();
    let itemIdx = 0;
    
    const todayCallHistory = user.usage?.find((val, idx) => {
        if(val.day == todayStr) {
            itemIdx = idx;
            return val;
        }
    });

    if(todayCallHistory && todayCallHistory.count >= MAX_DAILY_API_CALLS) {
        return "Max API call daily limit reached";    
    };

    let usageRecord = {"day": todayStr, "count": 1};
    if(!todayCallHistory) {
        user.usage = [];
        user.usage?.push(usageRecord);
    } else {
        todayCallHistory.count += 1;
        usageRecord = todayCallHistory;
        user.usage![itemIdx] = todayCallHistory;
    }
    
    return dummy_data;
}

The code for dummy_ex_rate.ts

export const dummy_data = {
    "eur": {
        "code": "EUR",
        "alphaCode": "EUR",
        "numericCode": "978",
        "name": "Euro",
        "rate": 0.94053966288128,
        "date": "Fri, 17 Mar 2023 23:55:01 GMT",
        "inverseRate": 1.0632193829408
    },
    "gbp": {
        "code": "GBP",
        "alphaCode": "GBP",
        "numericCode": "826",
        "name": "U.K. Pound Sterling",
        "rate": 0.82428107076564,
        "date": "Fri, 17 Mar 2023 23:55:01 GMT",
        "inverseRate": 1.213178411426
    },
    "jpy": {
        "code": "JPY",
        "alphaCode": "JPY",
        "numericCode": "392",
        "name": "Japanese Yen",
        "rate": 132.57002221947,
        "date": "Fri, 17 Mar 2023 23:55:01 GMT",
        "inverseRate": 0.0075431834683146
    },
    "aud": {
        "code": "AUD",
        "alphaCode": "AUD",
        "numericCode": "036",
        "name": "Australian Dollar",
        "rate": 1.4931177689981,
        "date": "Fri, 17 Mar 2023 23:55:01 GMT",
        "inverseRate": 0.66973953479303
    }
}

Conclusion

In this post you saw how to add API key authentication to your NodeJS API using express and Typescript. While this code uses NodeJS and Typescript, the concepts showcases here can be applied to any other language such as Javascript, Java etc. You can find all the code on my Github repo.

In the next post, the rate limiting concepts will be discussed in more detail.

If you find any of my posts useful and want to support me, you can buy me a coffee 🙂

https://www.buymeacoffee.com/bhumansoni

While you are here, maybe try one of my apps for the iPhone.

Products – My Day To-Do (mydaytodo.com)

Have a read of some of my other posts on AWS

Deploy NodeJS, Typescript app on AWS Elastic beanstalk – (mydaytodo.com)

How to deploy spring boot app to AWS & serve via https – My Day To-Do (mydaytodo.com)

Some of my other posts on Javascript …

What is Javascript event loop? – My Day To-Do (mydaytodo.com)

How to build a game using Vanilla Javascript – My Day To-Do (mydaytodo.com)

Vanilla Javascript: Create Radio Buttons (How-To) – Bhuman Soni (mydaytodo.com)

Java Spring Boot & Vanilla Javascript solution – My Day To-Do (mydaytodo.com)

Vanilla Javascript: Create Radio Buttons (How-To) – Bhuman Soni (mydaytodo.com)

Categories: Javascript

0 Comments

Leave a Reply

Avatar placeholder