DEV Community

Cover image for Authentication with node:http and better-auth
Daniel Madrid
Daniel Madrid

Posted on

Authentication with node:http and better-auth

Sometimes I just want a Node.js server that works without pulling in Express or any other heavy frameworks. This is a simple pattern I’ve been using for handling authentication with node:http and a library called better-auth. I’m writing this down for future me, so I don’t have to remember all the small details. Hope this helps you or at least shows you something new.

Why this approach?

  • No frameworks: I don’t want to add Express/Koa just for a few endpoints.
  • Minimal dependencies: better-auth handles the auth logic; everything else stays standard Node.
  • Control: I can see exactly what’s happening in the request/response pipeline.

The Base Request Handler

This is a representation of a http.Server on request handler that receives an incommingMessage and serverResponse in order to return a generic type <T> or void by default.

// request-handler.ts

import http from "node:http";

export type RequestHandler<ReturnType = void> = (
  incommingMessage: http.IncomingMessage,
  serverResponse: http.ServerResponse,
) => ReturnType;

Enter fullscreen mode Exit fullscreen mode

The Handler

The core of this setup is a small handler function that wraps your auth handler. It converts the raw request, runs the auth, sets headers, and streams the body if present.

// utils.ts

import http from "node:http";

export function incommingMessageToRequest(
  incommingMessage: http.IncomingMessage,
  baseUrl: URL,
): Request {
  const method = incommingMessage.method || "GET";

  const url = new URL(incommingMessage.url || "/", baseUrl);

  const headers = new Headers();
  for (const [key, value] of Object.entries(incommingMessage.headers)) {
    if (Array.isArray(value)) {
      value.forEach((v) => headers.append(key, v));
    } else if (value !== undefined) {
      headers.set(key, value);
    }
  }

  const body =
    method === "GET" || method === "HEAD" ? undefined : incommingMessage;

  return new Request(url.toString(), {
    method,
    headers,
    body,
    duplex: body ? "half" : undefined,
  });
}
Enter fullscreen mode Exit fullscreen mode
// better-auth-handler.ts

import stream from "node:stream";
import streamPromises from "node:stream/promises";

import type { Auth, BetterAuthOptions } from "better-auth";

import type { RequestHandler } from "./request-handler";

import * as Utils from "./utils";

export type Config = {
  baseUrl: URL;
};

export default function betterAuth(
  auth: Auth<BetterAuthOptions>,
  config: Config,
): RequestHandler<Promise<void>> {
  return async (incommingMessage, serverResponse) => {
    const response = await auth.handler(
      Utils.incommingMessageToRequest(incommingMessage, config.baseUrl),
    );

    serverResponse.statusCode = response.status;
    serverResponse.statusMessage = response.statusText;

    response.headers.forEach((value, key) => {
      if (key.toLowerCase() === "transfer-encoding") return;
      serverResponse.setHeader(key, value);
    });

    if (!response.body) {
      serverResponse.end();
      return;
    }

    await streamPromises.pipeline(
      stream.Readable.fromWeb(response.body),
      serverResponse,
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

Hooking it up to a server

Here’s how I use it in a plain Node HTTP server:

// index.ts

import http from "node:http";

import BetterAuthHandler from "./better-auth-handler";

import { auth } from "./auth";

async function main() {
  const httpServer = http.createServer();

  const betterAuthHandler = BetterAuthHandler(auth, {
    baseUrl: new URL(process.env.BETTER_AUTH_URL),
  });

  httpServer.on("request", async (incommingMessage, serverResponse) => {
    await betterAuthHandler(incommingMessage, serverResponse);

    if (serverResponse.writableEnded) {
      return;
    }

    serverResponse.statusCode = 404;
    serverResponse.end("Not Found");
    return;
  });

  httpServer.listen(env.PORT, () => {
    console.log(`http server listening on port ${env.PORT}`);
  });
}

main();
Enter fullscreen mode Exit fullscreen mode

That’s it. Nothing fancy, just a small, predictable setup that does what it needs to do.

If you’re reading this in the future: this worked, it stayed out of the way, and it didn’t require a framework.

Hopefully it’s useful to you too if you’re aiming for the same kind of minimal, boring-in-a-good-way Node server.

Top comments (0)