Moving to Next.js Serverless Functions for REST APIs

Share

Moving to Next.js Serverless Functions for REST APIs

I’ve recently embarked on rewriting Agora using NextJS 13 and it’s stirring up some serious déjà vu of my Rails and Django days back in 2007-2009. The premise of a framework-driven infrastructure r...

Ewakened @kentf

I’ve recently embarked on rewriting Agora using NextJS 13 and it’s stirring up some serious déjà vu of my Rails and Django days back in 2007-2009. The premise of a framework-driven infrastructure resonates with me. Coupled with the prospect of partnering with Vercel, I’m eagerly anticipating a leaner, more efficient developer experience and a highly performant web app.

One of the most exicting new features is serverless function support. This feature, specific to NextJS 13+, has the potential to dominate as its adoption becomes more widespread. The concept is elegantly simple:

Create an api directory within your app directory. Inside api, create another directory named v1, and within v1, create a users directory. Inside the users directory, create a file named routes.js. Your directory structure should look something like this:

app
├── api
│ └── v1
│ └── users
│ └── routes.js

I understand if this feels a tad convoluted, especially for those of us accustomed to Sinatra, Flask, and other router-based frameworks. Designating routes as folders might seem inefficient, but bear with me. It’s plausible that this folder structure may either fade away as a vestige of the old /pages architecture or prove beneficial from a hosting/infra standpoint. Time will tell.

Let’s say we want a function that displays all our users. We’d write something like this in route.js:

// src/app/api/v1/users/routes.js
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";

export async function GET(request) {

  const prisma = new PrismaClient();

  let page = parseInt(request.nextUrl.searchParams.get("page"), 10);
  if (isNaN(page) || page < 1) {
    page = 1;
  }

  const pageSize = 25;
  const total_count = await prisma.users.count();
  const total_pages = Math.ceil(total_count / pageSize);

  const proposals = await prisma.users.findMany({
    take: pageSize,
    skip: (page - 1) * pageSize,
  });

  await prisma.$disconnect();

  // Build out proposal response
  const response = {
    meta: {
      current_page: page,
      total_pages: total_pages,
      page_size: pageSize,
      total_count: total_count,
    },
    users: users.map((proposal) => ({
      // Just testing out, not meant for production
      id: user.id,
      username: user.username,
    })),
  };

  return NextResponse.json(response);
}

But what if we need to authenticate API users against this function? No problem.

Create a middleware directory within your src/app/lib and add something like this:

// src/app/lib/middlewear/authenticateAPIUser.js
import { NextResponse } from "next/server";
import { headers } from "next/headers";

export function authenticateAPIUser(request) {
  const headersList = headers();
  const apiKey = headersList.get("api-key");

  // Use your method of checking the API user's key
  const apiUser = validateAPIKey(apiKey);

  if (!apiUser || !apiUser.active) {
    return NextResponse.redirect(new URL("/api/forbidden", request.url));
  }

  return null;
}

Then go back and modify your route.js file to look like this:

// src/app/api/v1/users/routes.js
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { authenticateAPIUser } from "src/app/lib/middlewear/authenticateAPIUser"

export async function GET(request) {

  // Check if the session is authenticated first
  const authResponse = authenticateAPIUser(request);
  if (authResponse) {
    return authResponse;
  }

  const prisma = new PrismaClient();

  let page = parseInt(request.nextUrl.searchParams.get("page"), 10);
  if (isNaN(page) || page < 1) {
    page = 1;
  }

  const pageSize = 25;
  const total_count = await prisma.users.count();
  const total_pages = Math.ceil(total_count / pageSize);

  const proposals = await prisma.users.findMany({
    take: pageSize,
    skip: (page - 1) * pageSize,
  });

  await prisma.$disconnect();

  // Build out proposal response
  const response = {
    meta: {
      current_page: page,
      total_pages: total_pages,
      page_size: pageSize,
      total_count: total_count,
    },
    users: users.map((proposal) => ({
      // Just testing out, not meant for production
      id: user.id,
      username: user.username,
    })),
  };

  return NextResponse.json(response);
}

Make sure that your /app/api/forgidden route is set up to return a 403 status code and you are good to go, you can use something like this

// Forbidden route if API Autentication fails
import { NextResponse } from "next/server";

export async function GET(request) {
  return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

And put that in /src/app/api/forbidden/routes.js

There you have it. The ability to seamlessly scale out serverless functions—especially if you’re using the same hosting environment for both front-end and back-end (e.g., Vercel)—is a game-changer.

Stay tuned for more discoveries as I continue to explore this exciting frontier.

Read more

Introducing Runner

Introducing Runner

It was December 28th. I woke up at 6am and Cursor had been running all night. I had Claude working while I slept, writing code, pushing commits, doing things that would have taken me a full day. Just sitting there staring at the screen, coffee getting cold, and realizing life wasn't going to be the same. I got a taste of the agentic workflow and couldn't go back. Turns out I wasn't the only one. When the Christmas break ended, Charlie and Yitong, my co-founders at Agora, had similar epiphanies

By Kent Fenwick
How I build: Dec 2025 Edition

How I build: Dec 2025 Edition

I treat AI as a tool and like any tool, people can use the same tools in different ways to acheive different results. I recently realized that I use AI differently than some of my colleagues so I was encouraged to share how I code. Before I talk about the tools and tactics, it's important to remember that I have a golden rule when using AI to code: Don't get AI to build something you don't understand or haven't built at least 3 times before. CRUD application? ✅ Simple Tab View Mobile App? ✅

By Kent Fenwick
The Messy Middle

The Messy Middle

My family is moving into a new house and we wanted to do a little bit of remodelling upstairs. Nothing big, just closing off a wall, making a closet a little bigger and swapping out a sink in the bathroom. My wife's family owns a construction business, so we are lucky to have an inside track with contractors and knowledge. My father in law is a genius, he runs his own construction business but still made time to come and measure, sketch and draw up the plans himself. My wife and I his customers

By Kent Fenwick
Someone needs to hear it

Someone needs to hear it

So, you want to write something but you are feeling like you can’t finish and post it for a really simple reason: there are 6 people in the world smarter than you on this topic, surely they should write it not you. During an interview today for Agora’s product manager, a warrior of art reminded me of this mental block and it rung so true. How many great pieces of advice, lessons, ideas and inspiration are left unsaid due to someone feeling as though they aren’t qualified enough to talk about it

By Kent Fenwick