Back to journal
·2 min readBackend DevelopmentNodejsExpressSecurityApi ProxyBackend

Hide Your API Keys with an API Proxy Server

Move API keys off the client with a tiny Express proxy. Weather API example, axios, dotenv. Not perfect, but better than keys in the bundle.

ShareCopy failed

We've all dropped an API key in frontend code while prototyping. TMDB, OpenWeather, whatever. Keys in URLs or headers leak through DevTools, scraped builds, angry users.

API keys belong on the server. The browser talks to your proxy. Your proxy talks to the vendor with the secret in env vars.

This walkthrough uses OpenWeatherMap. Same pattern works elsewhere.

Setup

Grab a free API key from OpenWeather after signup.

cd ~/Documents/tutorials
mkdir api-proxy-server && cd api-proxy-server
npm init -y
npm install -S express cors dotenv axios
npm install -D nodemon
{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon --config nodemon.json"
  }
}

Project layout:

server/
├── src
│   ├── utils
│   │   └── env.js
│   └── routes
│       └── index.js
├── package.json
├── nodemon.json
├── index.js
└── .env

nodemon.json:

{
  "verbose": true,
  "ignore": ["node_modules/"],
  "watch": ["./**/*"]
}

.env (use your real key):

API_PORT = 1337
API_BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
API_KEY = "your-key-here"

src/utils/env.js:

require("dotenv").config();

module.exports = {
  port: Number(process.env.API_PORT) || 3000,
  baseURL: String(process.env.API_BASE_URL) || "",
  apiKey: String(process.env.API_KEY) || "",
};

index.js:

const express = require("express");
const cors = require("cors");
const env = require("./src/utils/env");

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cors());

app.use("/api", require("./src/routes"));

app.all("*", (request, response, next) => {
  return response.status(404).json({ message: "Endpoint not found!" });
});

app.listen(env.port, () => {
  console.log(`Server is up and running at http://localhost:${env.port}`);
});

Route handler:

const router = require("express").Router();
const { default: axios } = require("axios");
const env = require("../utils/env");

router.get("/", async (request, response, next) => {
  try {
    const query = request.query || {};
    const params = {
      appid: env.apiKey,
      ...query,
    };
    const { data } = await axios.get(env.baseURL, { params });

    return response.status(200).json({
      message: "Current weather data fetched!",
      details: { ...data },
    });
  } catch (error) {
    const {
      response: { data },
    } = error;
    const statusCode = Number(data.cod) || 400;
    return response.status(statusCode).json({ message: "Bad Request", details: { ...data } });
  }
});

module.exports = router;

Before: https://api.openweathermap.org/...?q=Thessaloniki&appid=SECRET

After: http://localhost:1337/api?q=Thessaloniki and the server injects appid.

Tradeoffs

You maintain another service. Latency adds a hop. For public read-only weather data, some teams accept client keys with rate limits. For anything billable or private, proxy anyway.

Source: GitHub repo

Next ideas I never shipped: response caching and rate limiting so one angry user can't burn your quota. Tell me if you've solved that differently.

ShareCopy failed