Lessons from FundedFilter on Vercel

While developing FundedFilter on React/Vite and deploying to Vercel, there are several confusing steps involved which I am outlining below for the sake of less confusion in the future.

Env Vars don’t rely on .env

ENV Vars need to be added manually under /Settings on Vercel.

Vite ENV Vars need a VITE_ prefix to be used locally, like below.

Routing for Vercel works differently from locally

A vercel.json file needs to be made including the following, in order for routing on a navigation bar (also shown below):

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

The NavigationBar needs to use Link as shown


import {Link} from 'react-router-dom';

  // Use appropriate component based on link type
  return isExternal ? (
    <a 
      href={href}
      className={`
        flex items-center gap-2 px-5 py-2 rounded-md text-sm font-medium transition-colors
        ${isActive 
          ? "bg-primary text-white shadow-sm" 
          : "text-gray-700 hover:bg-gray-100 hover:text-primary"}
      `}
      target="_blank"
      rel="noopener noreferrer"
    >
      <span>{label}</span>
    </a>
  ) : (
    <Link 
      to={href}
      className={`
        flex items-center gap-2 px-5 py-2 rounded-md text-sm font-medium transition-colors
        ${isActive 
          ? "bg-primary text-white shadow-sm" 
          : "text-gray-700 hover:bg-gray-100 hover:text-primary"}
      `}
    >
      <span>{label}</span>
    </Link>
  );
};

{/* Logo */}
          <div className="flex items-center">
            <Link to="/" className="font-bold text-xl text-primary">
              FundedFilter
            </Link>
          </div>

API Handlers and Serverless Proxy Functions

Something else new about this arrangement is the way API calls can be handled more securely.

With Vercel, I have created a folder outside the /src folder where custom api handlers can be made, and they don’t have to be imported inside the files that use them. Vercel will allow us to call our api handler with some code like the following, and it will automatically call the handler function and deal with the API call, so our front end doesn’t have to contain the code and potentially expose what our API route or key variable names are.

API Proxy File

export default async function handler(req, res) {
  // Set CORS headers
  setCorsHeaders(res);
  
  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }
  
  // Get endpoint from query parameters
  const endpoint = req.query.endpoint;
  
  // Handle test endpoint
  if (endpoint === 'test') {
    return handleTestEndpoint(req, res);
  }
  

API Client (for local routing)

const DEV = import.meta.env.DEV;

const PROD_ENDPOINTS = {
  accounts: '/api/proxy?endpoint=acc',
  health: '/api/proxy?endpoint=sage',
  contact: '/api/proxy?endpoint=email',
  test: '/api/proxy?endpoint=test'
};

const DEV_ENDPOINTS = {
  accounts: 'https://URL/api/acc',
  contact: 'https://URL/api/email',
  health: 'https://URL/sage'
};

const API_ROUTES = DEV ? DEV_ENDPOINTS : PROD_ENDPOINTS;

export const apiClient = {
  async fetchAccounts() {

The setup is such that when testing and developing locally, we will use the apiClient file which is technically exposed, but then use the proxy file when the app is deployed on Vercel.

In Index.tsx, we would have the following, which would call apiClient and try to fetch the data (which works locally). But when it’s run in production, apiClient would simply pass the call through proxy, which would check the endpoint and make the call.

  // Use React Query to fetch accounts
  const { data: accounts = [], isLoading, error } = useQuery({
    queryKey: ['accounts'],
    queryFn: apiClient.fetchData
  });

By using .gitignore to ignore the api-client, it also will not upload this file to github. But if it’s already been uploaded, gitignore doesn’t seem to apply.

Passing UserID through API requests – Serverless Complications Again

First, I tried using IP checking with the favorites feature, storing the IP in the db and checking IP at initial load when passing accounts information to the user. But IP doesn’t easily get passed because of Vercel’s serverless functionality.

Claude suggested using cookies/IP, and this wasn’t working for whatever reason, so I went on to localStorage. This did work successfully when trying on my locally hosted app, but the proxy was not processing the new method.

At first, while passing this URL into the proxy, it was running into some unlogged bug, because the endpoint was not able to handle the ? before userId.

const userId = apiClient.getUserId();

    const response = await fetch(`${API_ROUTES.accounts}&userId=${userId}`, {headers});

It turns out, since our production endpoints start with ?, we need to switch to & for arguments. Once I did this, it worked again.

Next, the userId is not passed the same way for the favorite toggling feature. So we can’t do &userId=${userId}. Or we can, but we don’t need to. Instead, we pass the userId in the body, like below.

const userId = apiClient.getUserId();
      const response = await fetch(API_ROUTES.favorites, {
        method: 'POST',
        headers,
        body: JSON.stringify({
          ...data,
          userId // Add user ID to the request body
        })
      });

Another issue was that Claude suggested using “this.getUserId()”. And this doesn’t work. Perhaps it’s because of the lack of a try catch, but this was caught by GPT, who suggested using “clientApi.getUserId()”. After that, it worked.

Finally, the backend. We retrieve the userId from the req.query as below for the account fetch, and then from the body for the toggling feature.

const userId = req.query.userId || "";
vs.
const { firm, accountName, userId } = req.body;

This userId is then passed into our functions and used in our query to match the favorited accounts with the user.

One potential issue is that different devices will be treated as different users, because of the localStorage usage.

Leave a Reply

Your email address will not be published. Required fields are marked *