TypeScript Backend Frameworks

Express, Hono, Elysia, NestJS, tRPC, oRPC & Bun β€” routes, middleware, type safety & patterns

Framework / Backend
Contents
πŸš‚

Express (with TypeScript)

// npm i express && npm i -D @types/express tsx
import express, { Request, Response, NextFunction } from "express";

const app = express();
app.use(express.json());

// Typed route handler
interface User { id: number; name: string; email: string; }

app.get("/users", (req: Request, res: Response) => {
  const page = Number(req.query.page) || 1;
  res.json({ users: [], page });
});

app.post("/users", (req: Request<{}, {}, Omit<User, "id">>, res) => {
  const { name, email } = req.body;
  res.status(201).json({ id: 1, name, email });
});

app.get("/users/:id", (req: Request<{ id: string }>, res) => {
  const id = parseInt(req.params.id);
  res.json({ id });
});

// Middleware
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: "Unauthorized" });
  next();
};

app.use("/api", authMiddleware);

// Error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({ error: err.message });
});

app.listen(3000, () => console.log("Express on :3000"));
πŸ”₯

Hono

// npm i hono  |  Works on Node, Bun, Deno, CF Workers, Vercel Edge
import { Hono } from "hono";
import { cors } from "hono/cors";
import { jwt } from "hono/jwt";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono();

// Middleware
app.use("*", cors());
app.use("/api/*", jwt({ secret: "my-secret" }));

// Routes with type-safe validation
const createUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

app.get("/users", (c) => {
  const page = c.req.query("page") || "1";
  return c.json({ users: [], page: Number(page) });
});

app.post("/users", zValidator("json", createUserSchema), (c) => {
  const data = c.req.valid("json");  // fully typed!
  return c.json({ id: 1, ...data }, 201);
});

app.get("/users/:id", (c) => {
  const id = c.req.param("id");
  return c.json({ id });
});

// Route groups
const api = new Hono();
api.get("/health", (c) => c.text("OK"));
app.route("/api", api);

// Error handling
app.onError((err, c) => {
  return c.json({ error: err.message }, 500);
});

export default app;  // for Bun/CF Workers
// Or: serve({ fetch: app.fetch, port: 3000 })
🦊

Elysia (Bun-native)

// bun add elysia @elysiajs/swagger
import { Elysia, t } from "elysia";
import { swagger } from "@elysiajs/swagger";

const app = new Elysia()
  .use(swagger())

  // Type-safe route with validation
  .get("/users", ({ query }) => {
    return { users: [], page: query.page };
  }, {
    query: t.Object({ page: t.Number({ default: 1 }) })
  })

  .post("/users", ({ body }) => {
    return { id: 1, ...body };
  }, {
    body: t.Object({
      name: t.String({ minLength: 2 }),
      email: t.String({ format: "email" }),
    }),
    response: t.Object({
      id: t.Number(),
      name: t.String(),
      email: t.String(),
    }),
  })

  .get("/users/:id", ({ params }) => {
    return { id: params.id };
  }, {
    params: t.Object({ id: t.Number() })
  })

  // Middleware (derive/guard)
  .derive(({ headers }) => {
    const token = headers.authorization;
    return { userId: verifyToken(token) };
  })

  // Groups
  .group("/api", (app) =>
    app.get("/health", () => "OK")
  )

  .listen(3000);

console.log(`Elysia running at ${app.server?.url}`);
🐱

NestJS

// npx @nestjs/cli new my-project
// Module + Controller + Service architecture

// ── user.entity.ts ──
export class User {
  id: number;
  name: string;
  email: string;
}

// ── create-user.dto.ts ──
import { IsString, IsEmail, MinLength } from "class-validator";

export class CreateUserDto {
  @IsString() @MinLength(2)
  name: string;

  @IsEmail()
  email: string;
}

// ── users.service.ts ──
import { Injectable } from "@nestjs/common";

@Injectable()
export class UsersService {
  private users: User[] = [];

  findAll(): User[] { return this.users; }
  findOne(id: number): User | undefined {
    return this.users.find(u => u.id === id);
  }
  create(dto: CreateUserDto): User {
    const user = { id: Date.now(), ...dto };
    this.users.push(user);
    return user;
  }
}

// ── users.controller.ts ──
import { Controller, Get, Post, Body, Param, UseGuards } from "@nestjs/common";

@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() { return this.usersService.findAll(); }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.usersService.findOne(+id);
  }

  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
}

// ── users.module.ts ──
import { Module } from "@nestjs/common";

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

// Guards (auth)
@UseGuards(AuthGuard)
@Get("profile")
getProfile() { ... }
⚑

tRPC

// npm i @trpc/server @trpc/client zod

// ── server/trpc.ts ── (initialize)
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.context<{ userId?: string }>().create();

export const router = t.router;
export const publicProcedure = t.procedure;

// Protected procedure
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { userId: ctx.userId } });
});

// ── server/routers/users.ts ──
export const usersRouter = router({
  list: publicProcedure.query(async () => {
    return db.user.findMany();
  }),

  byId: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return db.user.findUnique({ where: { id: input.id } });
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      return db.user.create({ data: { ...input, createdBy: ctx.userId } });
    }),
});

// ── server/index.ts ── (root router)
export const appRouter = router({
  users: usersRouter,
});
export type AppRouter = typeof appRouter;

// ── client usage (React example) ──
import { createTRPCReact } from "@trpc/react-query";
const trpc = createTRPCReact<AppRouter>();

// In component β€” fully type-safe!
const { data } = trpc.users.list.useQuery();
const mutation = trpc.users.create.useMutation();
🌐

oRPC

// npm i @orpc/server @orpc/client
// Type-safe RPC β€” framework-agnostic, OpenAPI-native

import { os } from "@orpc/server";
import { z } from "zod";

// Define procedures
const listUsers = os
  .input(z.object({ page: z.number().optional() }))
  .handler(async ({ input }) => {
    return db.user.findMany({ skip: ((input.page ?? 1) - 1) * 20, take: 20 });
  });

const createUser = os
  .input(z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }))
  .handler(async ({ input }) => {
    return db.user.create({ data: input });
  });

// Router
const appRouter = os.router({
  users: os.router({
    list: listUsers,
    create: createUser,
  }),
});

// Serve (works with any HTTP framework)
import { createServer } from "@orpc/server/node";
createServer({ router: appRouter }).listen(3000);

// Client
import { createORPCClient } from "@orpc/client";
const client = createORPCClient<typeof appRouter>({ baseURL: "http://localhost:3000" });

const users = await client.users.list({ page: 1 });  // typed!
🍞

Bun Runtime

// Bun has a built-in HTTP server β€” no framework needed
Bun.serve({
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);

    if (url.pathname === "/users" && req.method === "GET") {
      return Response.json({ users: [] });
    }

    if (url.pathname === "/users" && req.method === "POST") {
      const body = await req.json();
      return Response.json({ id: 1, ...body }, { status: 201 });
    }

    return new Response("Not Found", { status: 404 });
  },
});

// Bun utilities
const file = Bun.file("data.json");
const text = await file.text();
await Bun.write("output.txt", "Hello");

// SQLite (built-in!)
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
const users = db.query("SELECT * FROM users").all();

// Password hashing (built-in!)
const hash = await Bun.password.hash("mypassword");
const valid = await Bun.password.verify("mypassword", hash);

// Package manager: bun install, bun add, bun remove
// Runner: bun run script.ts, bunx create-next-app
βš–οΈ

Framework Comparison

Framework Runtime Style Best For
Express Node Minimal Mature ecosystem, flexible
Hono Any Minimal Edge/serverless, ultra-fast
Elysia Bun Minimal Performance, end-to-end types
NestJS Node Opinionated Enterprise, DI, Angular-like
tRPC Any RPC Full-stack TS, type inference
oRPC Any RPC OpenAPI-native, framework-agnostic