# Building a REST API with Vercel Functions: From Concept to Production

**Author:** kelexine  
**Date:** 2025-12-11  
**Category:** Engineering  
**Tags:** API, Vercel, JavaScript, Serverless, Vite, Development  
**URL:** https://kelexine.is-a.dev/blog/building-rest-api-vercel-functions

---

## The Beginning

It started with a simple Question: "Can you build a REST API to query and pull information from the website?" What seemed like a straightforward task evolved into an interesting journey through serverless architecture, first with Cloudflare Workers, and eventually migrating to **Vercel Serverless Functions** to better align with my hosting environment.

## Understanding the Landscape

My blog is built as a static site using React, Vite, and MDX, deployed on Vercel. The content is primarily stored in three formats:

1. **Posts**: MDX files with YAML frontmatter, compiled into a JSON index at build time
2. **TILs** (Today I Learned): Individual JSON files, aggregated into an index
3. **Projects**: A static JavaScript array with project information

The goal was to expose this data through a RESTful API without adding complex server infrastructure.

## The Architecture Transition

While I initially used Cloudflare Workers, moving the API to Vercel made more sense as it keeps the frontend and backend logic in the same repository and deployment pipeline. Vercel's directory-based routing (`api/`) integrates seamlessly with the frontend. (That's all a lie, i actually lost my cloudflare account login credentials)

## API Reference

The API is designed to be intuitive and strictly RESTful. Here is a breakdown of the available endpoints.

### Posts API
`GET /api/posts`

| Query Param | Description |
| :--- | :--- |
| `page` | Pagination page (default: 1) |
| `limit` | Items per page (default: 10) |
| `q` | Search query for titles and content |

`GET /api/posts/:slug`
Retrieves a single post by its slug.

`GET /api/posts/category/:category`
Filters posts by category (e.g., `Engineering`, `Life`).

`GET /api/posts/tag/:tag`
Filters posts by specific tag.

**Response Example:**
```json
{
  "success": true,
  "data": [
    {
      "title": "Building a REST API...",
      "slug": "building-rest-api-vercel-functions",
      "category": "Engineering",
      "tags": ["API", "Vercel"],
      "date": "2025-12-11"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 42,
    "totalPages": 5
  }
}
```

### Projects API
`GET /api/projects`

| Query Param | Description |
| :--- | :--- |
| `featured` | Set to `true` to get only featured projects |
| `category` | Filter by project category |
| `q` | Search projects by tech stack or name |

`GET /api/projects/:id`
Retrieves a specific project by its ID.

### TIL (Today I Learned) API
`GET /api/til`

| Query Param | Description |
| :--- | :--- |
| `tag` | Filter TILs by tag |
| `page` | Pagination page |

`GET /api/til/recent`
Quickly fetch the 5 most recent TILs.

### Metadata
- `GET /api/categories`: Returns a list of all unique categories.
- `GET /api/tags`: Returns a unified list of tags from both Posts and TILs.

### Newsletter
`POST /api/newsletter/subscribe`
Accepts a JSON body with `email` to subscribe to the newsletter.

```json
{
  "email": "user@example.com"
}
```

## The Data Access Challenge

The blog's data is generated at build time. The frontend imports these JSON files directly, but the API handlers need to fetch them. Since we are in a serverless environment, we treat the static data as an internal resource.

We created a shared library `api/_lib/data.js` that abstracts the data fetching logic:

```javascript
// api/_lib/data.js
async function fetchJSON(path) {
    const protocol = process.env.NODE_ENV === 'development' ? 'http' : 'https';
    const host = process.env.VERCEL_URL || 'localhost:5173';
    const baseUrl = `${protocol}://${host}`;
    
    // Dynamically fetch from localhost or production URL
    const response = await fetch(`${baseUrl}${path}`);
    if (!response.ok) return null;
    return await response.json();
}
```

## Implementation Details: Vercel Functions

Unlike the Service Worker syntax of Cloudflare, Vercel Functions use the standard Node.js request/response pattern.

Here is how we handle the `posts` endpoint using a catch-all route:

```javascript
// api/posts/[...path].js
import { getPosts, getPostBySlug } from '../_lib/data.js';

export default async function handler(req, res) {
    const { path } = req.query; // Catch-all captures /api/posts/slug

    // Set Caching Headers
    res.setHeader('Cache-Control', 's-maxage=600, stale-while-revalidate=300');

    // Root: /api/posts
    if (!path || path.length === 0) {
        const page = req.query.page || 1;
        const result = await getPosts(page);
        return res.status(200).json({ success: true, ...result });
    }

    const [first, second] = path;

    // Slug: /api/posts/my-post-slug
    if (first && !second) {
        const post = await getPostBySlug(first);
        if (!post) return res.status(404).json({ error: 'Not Found' });
        return res.status(200).json({ success: true, data: post });
    }
    
    // ... handle categories, tags, etc.
}
```

## The "Catch-All" Routing Challenge

One significantly tricky part was routing. In Next.js, you can use `[[...path]].js` for "optional catch-all" routes, which handles both `/api/posts` and `/api/posts/some-slug`.

However, since this is a **Vite** project deployed as **Vercel Functions**, that specific Next.js syntax isn't fully supported for the root path. We initially faced `404 Not Found` errors when hitting `/api/posts` directly, or weird behaviors where the optional segments weren't being recognized.

### The Fix: Explicit Rewrites

To solve this, we renamed our files to the standard `[...path].js` (Standard Catch-all) and added explicit rewrite rules in `vercel.json`. Order matters here: specific rules must come *before* generic ones.

```json
{
    "rewrites": [
        { "source": "/api/posts", "destination": "/api/posts/[...path]" },
        { "source": "/api/projects", "destination": "/api/projects/[...path]" },
        { "source": "/api/til", "destination": "/api/til/[...path]" },
        { "source": "/api/(.*)", "destination": "/api/$1" }
    ]
}
```

This ensures that a request to `/api/posts` is correctly routed to the catch-all handler, just like `/api/posts/slug` is, essentially mimicking the "optional catch-all" behavior of Next.js but with explicit configuration.

## Performance and Caching

We utilize Vercel's Edge Caching logic with `Cache-Control` headers.
`s-maxage=600, stale-while-revalidate=300` means:
- The content is cached at the edge for 10 minutes (600s).
- For the next 5 minutes (300s), users get the stale content while the cache updates in the background.

This ensures the API is blazing fast while staying relatively up-to-date.

## Key Takeaways

1. **Framework nuances matter**: "Optional catch-all" syntax is specific to Next.js; standard functions require explicit configuration.
2. **Routing configuration**: `vercel.json` is powerful for handling legacy or complex routing needs in a serverless app.
3. **Shared Logic**: Extracting the data fetching logic into `_lib/` made the handlers clean and focused on routing.

The API is now live at `https://kelexine.is-a.dev/api/`. Feel free to explore!

---

*This content is available at [kelexine.is-a.dev/blog/building-rest-api-vercel-functions](https://kelexine.is-a.dev/blog/building-rest-api-vercel-functions)*
