Skip to main content

Sharing API endpoints across client and server

Recently I was working on a demo project with React/TypeScript as front-end (running on port 5173) and Express as backend (running on port 5000). Front end codes is built into a static folder and served by Express.

I’m using Vite, and I’ve set up a proxy inside Vite, so front end codes can just use /api/2fa/enable instead of needing to pass the whole server url like http://localhost:5000.

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:5000",
        changeOrigin: true,
      },
    },
  },
});

Although there is still a tiny issue I want to improve, that is a single API endpoint will first be defined by server and then called by client, something like this:

./server/server.js
// Two-Factor Authentication
app.post('/api/2fa/enable', enableTwoFactorAuth);
app.post('/api/2fa/verify', verifyTwoFactorAuth);
app.post('/api/2fa/disable', disableTwoFactorAuth);
app.post('/api/2fa/login', loginWithTwoFactorAuth);
./client/TwoFactorAuth.tsx
// ...
const fetchQRCode = async () => {
  const res = await fetch('/api/2fa/enable', {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ username: user.username }),
  });
  // ...
};

It works ok, but it would be nice if we can creat a shared apiRoutes.json file to store all API routes which then can be consumed by client and server, and that is exactly what I did, and it does make things a bit easier whenever I want to update the endpoint as I only need to update apiRoutes.json file once.

./shared/apiRoutes.json
{
  "TWO_FACTOR_AUTH_ENABLE": "/api/2fa/enable",
  "TWO_FACTOR_AUTH_VERIFY": "/api/2fa/verify",
  "TWO_FACTOR_AUTH_DISABLE": "/api/2fa/disable",
  "TWO_FACTOR_AUTH_LOGIN": "/api/2fa/login"
  // ... other routes
}
./server/server.js
// As of the time I wrote this blog, importing JSON modules is an experimental
// feature and might change at any time
import apiRoutes from "./shared/apiRoutes.json" assert { type: "json" };

// Two-Factor Authentication
app.post(apiRoutes.TWO_FACTOR_AUTH_ENABLE, enableTwoFactorAuth);
app.post(apiRoutes.TWO_FACTOR_AUTH_VERIFY, verifyTwoFactorAuth);
app.post(apiRoutes.TWO_FACTOR_AUTH_DISABLE, disableTwoFactorAuth);
app.post(apiRoutes.TWO_FACTOR_AUTH_LOGIN, loginWithTwoFactorAuth);
./client/TwoFactorAuth.tsx
// ...
import apiEndpoints from "../../shared/apiRoutes.json";

const fetchQRCode = async () => {
  const res = await fetch(apiEndpoints.TWO_FACTOR_AUTH_ENABLE, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ username: user.username }),
  });
  // ...
};