Skip to content

Node.js Server Proxy

Hide your mfd_srv_* key + cache tiles. Works for any Node framework — Express, Fastify, Hono, Koa, plain http.

Express

js
import express from 'express';
import { createHmac } from 'node:crypto';

const app = express();
const KEY    = process.env.MFD_SRV_KEY;
const SECRET = process.env.MFD_SRV_SECRET;

function sign(method, path) {
  const ts  = String(Math.floor(Date.now() / 1000));
  const sig = createHmac('sha256', SECRET).update(`${method}\n${path}\n${ts}`).digest('hex');
  return { 'X-MFD-Timestamp': ts, 'X-MFD-Signature': sig, Authorization: `Bearer ${KEY}` };
}

app.get('/tiles/:z/:x/:y.pbf', async (req, res) => {
  const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
  const r    = await fetch(`https://tiles.mapsfordevs.com${path}`, { headers: sign('GET', path) });
  if (!r.ok) return res.status(r.status).end();

  res.set('Content-Type', 'application/x-protobuf');
  res.set('Cache-Control', 'public, max-age=86400, immutable');
  for await (const chunk of r.body) res.write(chunk);
  res.end();
});

app.listen(8080);

Fastify

js
import Fastify from 'fastify';
import { createHmac } from 'node:crypto';

const app = Fastify({ logger: true });

app.get('/tiles/:z/:x/:y.pbf', async (req, reply) => {
  const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
  const ts   = String(Math.floor(Date.now() / 1000));
  const sig  = createHmac('sha256', process.env.MFD_SRV_SECRET)
                .update(`GET\n${path}\n${ts}`).digest('hex');

  const upstream = await fetch(`https://tiles.mapsfordevs.com${path}`, {
    headers: {
      Authorization: `Bearer ${process.env.MFD_SRV_KEY}`,
      'X-MFD-Timestamp': ts,
      'X-MFD-Signature': sig
    }
  });

  reply.header('Content-Type', 'application/x-protobuf');
  reply.header('Cache-Control', 'public, max-age=86400');
  return reply.send(upstream.body);
});

app.listen({ port: 8080 });

Hono (Cloudflare Workers / Bun / Node)

ts
import { Hono } from 'hono';

const app = new Hono();

app.get('/tiles/:z/:x/:y.pbf', async (c) => {
  const { z, x, y } = c.req.param();
  const path = `/tiles/${z}/${x}/${y}.pbf`;
  const ts   = String(Math.floor(Date.now() / 1000));

  const enc = new TextEncoder().encode(`GET\n${path}\n${ts}`);
  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(c.env.MFD_SRV_SECRET),
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
  );
  const sigBytes = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc));
  const sig      = Array.from(sigBytes).map(b => b.toString(16).padStart(2, '0')).join('');

  const upstream = await fetch(`https://tiles.mapsfordevs.com${path}`, {
    headers: {
      Authorization: `Bearer ${c.env.MFD_SRV_KEY}`,
      'X-MFD-Timestamp': ts,
      'X-MFD-Signature': sig
    },
    cf: { cacheTtl: 86400, cacheEverything: true }
  });

  return new Response(upstream.body, {
    status: upstream.status,
    headers: {
      'Content-Type': 'application/x-protobuf',
      'Cache-Control': 'public, max-age=86400'
    }
  });
});

export default app;

In-memory tile cache (Express + LRU)

js
import LRU from 'lru-cache';
const cache = new LRU({ max: 10_000, ttl: 1000 * 60 * 60 * 24 });

app.get('/tiles/:z/:x/:y.pbf', async (req, res) => {
  const path = `/tiles/${req.params.z}/${req.params.x}/${req.params.y}.pbf`;
  const hit  = cache.get(path);
  if (hit) {
    res.set('Content-Type', 'application/x-protobuf');
    res.set('X-Cache', 'HIT');
    return res.end(hit);
  }
  const r   = await fetch(`https://tiles.mapsfordevs.com${path}`, { headers: sign('GET', path) });
  const buf = Buffer.from(await r.arrayBuffer());
  if (r.ok) cache.set(path, buf);
  res.set('Content-Type', 'application/x-protobuf').set('X-Cache', 'MISS').end(buf);
});

Don't

  • Don't put mfd_srv_* in front-end code, environment variables exposed to the browser, or CDN config.
  • Don't use the same key in dev + prod. Issue separate keys, restrict prod by IP.
  • Don't cache 4xx / 5xx responses.