A serverless-first, full-stack API framework for Deno — file-based routing, a powerful hooks system, first-class Zod validation, MongoDB integration, and a composable plugin architecture.
Thunder lets you build type-safe HTTP APIs fast, then ship them anywhere — from serverless platforms to a long-running server — without changing your code. It stays minimal at its core and grows through plugins.
- Why Thunder
- Features
- Requirements
- Quick Start
- Project Structure
- Core Concepts
- Environment Variables
- Plugins
- Available Tasks
- Code Generation
- Deployment
- Documentation
- Contributing
- Code of Conduct
- License
Most frameworks force a trade-off between "batteries included" and "stays out of your way." Thunder picks both: a tiny, predictable core plus an ecosystem of installable plugins that merge their routes, hooks, and models straight into your app.
- Serverless-first by design. Stateless requests, fast cold starts, no hidden startup work. Database indexes and migrations run via explicit scripts — never on boot.
- Type-safe end to end. Zod schemas double as runtime validators, OpenAPI specs, and generated TypeScript SDKs.
- Convention over configuration. Drop a file in
routes/, export aRouter, and it's live. - A project is a plugin. Any Thunder project can be published to GitHub and installed into another. No special packaging.
- 🗂 File-based routing —
routes/users.tsautomatically serves/users/*. - 🪝 Hooks system — pre/post request interceptors with priority-based ordering for auth, logging, rate limiting, and more.
- ✅ Zod validation — validate params, query, and body; one schema powers runtime checks, OpenAPI, and SDKs.
- 🍃 MongoDB integration — typed collections, transactions, and a
createCRUDhelper for instant REST endpoints. - 🧩 Plugin architecture — compose features (auth, payments, email, ...) by installing other Thunder projects.
- 🛠 Code generation — OpenAPI spec, TypeScript SDK, and a ready-made dashboard app.
- 🔌 Utilities included — env management, caching, structured logging, hashing, template rendering, pagination.
- Deno v2 or newer
- A MongoDB connection string (local or hosted, e.g. MongoDB Atlas)
# 1. Clone the framework as a new project
git clone https://github.com/Huruf-Tech/thunder.git my-app
cd my-app
# 2. Initialize the project
# ⚠️ Only run this on a fresh clone — it resets git history and sets up env.
deno task init
# 3. Configure your database connection
echo "DATABASE_URL=mongodb://localhost:27017/my-db" > .env
# 4. (Recommended) Install the official core plugin
# Adds auth, RBAC, CORS, security headers, and user management.
deno task add:plugin -n Huruf-Tech/thunder-core
# 5. Start the development server with hot reload
deno task devYour API is now running at http://localhost:8000.
thunder/
├── routes/ # Route handlers (file-based routing)
│ ├── index.ts # Fallback router (handles /)
│ └── {name}.ts # Route modules (e.g., users.ts → /users/*)
├── hooks/ # Pre/post request hooks (interceptors)
├── plugins/ # Installed plugins ({org}/{name}/)
├── schemas/ # MongoDB collection schemas with Zod validation
├── scripts/ # One-time setup scripts (indexes, migrations, seeds)
├── workers/ # (Optional) Background workers for separate deployment
├── public/www/ # Static assets / web root
├── core/ # Framework internals — DO NOT MODIFY
├── serve.ts # Server entry point
├── serve.base.ts # Request handler setup — DO NOT MODIFY
├── database.ts # MongoDB connection instance
├── .env # Environment variables
└── deno.json # Project configuration & tasks
Never edit
core/orplugins/. They are managed by the framework and overwritten bydeno task update:core/update:plugin. Build on top usingroutes/,hooks/,schemas/, andscripts/instead.
Each file in routes/ exports a Router. The filename becomes the route prefix, and only top-level .ts files are registered (sub-folders are ignored and can be used for organization).
// routes/foo.ts
import { Router } from "@/core/http/router.ts";
export default new Router("/api", function foo(router) {
// GET /foo/api
router.get("/", function hello() {
return () => Response.json({ message: "Hello" });
});
// GET /foo/api/users/:id
router.get("/users/:id", function getUser() {
return (req) => Response.json({ id: "123" });
});
}).group("Foo");Two rules to remember:
- Named functions are required for both the router callback and every handler (arrow functions throw an error).
- The prepare pattern: the outer handler function runs once at registration; the inner function it returns runs per request.
Supported methods: get, post, put, patch, del (note: del, not delete), and all. Path parameters use path-to-regexp syntax (:id, :id?, {/*path}, {/:id}).
Return an object with shape() and handler() to register validators. The shape() function feeds runtime validation and OpenAPI/SDK generation.
import { Router } from "@/core/http/router.ts";
import { bodyAsJson } from "@/core/http/utils.ts";
import z from "zod";
export default new Router("/api", function users(router) {
router.post("/users", function createUser() {
const $body = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const $return = z.object({ id: z.string(), name: z.string() });
return {
shape: () => ({ body: $body, return: $return }),
handler: async (req) => {
const body = $body.parse(await bodyAsJson(req));
return Response.json(
{ id: "123", name: body.name } satisfies z.output<typeof $return>,
);
},
};
});
}).group("Users");import { Response } from "@/core/http/response.ts";
Response.ok(); // 200
Response.json({ data: "value" }); // 200 JSON
Response.created({ id: "123" }); // 201
Response.badRequest(); // 400
Response.unauthorized(); // 401
Response.forbidden(); // 403
Response.notFound(); // 404
Response.redirect("/path"); // 302Errors are caught and formatted automatically: ZodError → 400 with details, a thrown Response is returned as-is, and any other Error → 500.
Hooks intercept requests before/after handlers. Drop a file in hooks/. Higher priority numbers run first.
// hooks/auth.ts
import type { THook } from "@/core/http/hooks.ts";
export default {
priority: 100, // runs early
pre: async ({ req }) => {
const token = req.headers.get("Authorization");
if (!token) return Response.unauthorized();
// Return a Response to short-circuit, or void to continue.
},
post: async ({ req, res }) => {
// Return a Response to override, or void to keep the original.
},
} satisfies THook;Define typed MongoDB collections with Zod in schemas/.
// schemas/user.ts
import z from "zod";
import { mongodb } from "@/database.ts";
const $userInput = z.object({
name: z.string(),
email: z.string().email(),
});
export const $user = $userInput.extend({
createdAt: z.date().default(() => new Date()),
});
export const userModel = mongodb.db().collection<z.infer<typeof $user>>("users");import { userModel, $user } from "@/schemas/user.ts";
// strictParse validates and applies defaults before insert
await userModel.insertOne($user.strictParse({ name: "John", email: "john@mail.com" }));
await userModel.findOne({ _id });Generate a full REST resource in a few lines:
// routes/users.ts
import { Router } from "@/core/http/router.ts";
import { createCRUD } from "@/core/utils/createCRUD.ts";
import { userModel, $user, $userInput } from "@/schemas/user.ts";
export default new Router("/api", function users(router) {
createCRUD({
router,
schema: $user,
model: userModel,
insertSchema: $userInput,
});
}).group("Users");This produces POST, GET (list, get-one, count), PATCH, and DELETE endpoints, with optional per-endpoint disabling, field isolation for multi-tenancy, and lifecycle hooks.
Thunder loads .env, plus .env.<environment> based on ENV_TYPE (development, production, test).
# Database connection (required)
DATABASE_URL=mongodb://username:password@localhost:27017/my-db
# Environment type
ENV_TYPE=developmentRead them in code via the Env helper:
import { Env, EnvType } from "@/core/utils/env.ts";
if (Env.is(EnvType.PRODUCTION)) { /* ... */ }
const dbUrl = await Env.get("DATABASE_URL");
const port = await Env.number("PORT");A Thunder project is a plugin — publish it to GitHub and install it elsewhere. Plugins live under plugins/{org}/{name}/, and their routes and hooks auto-merge into the host app.
# Install (optionally run its setup lifecycle: indexes, seeds, migrations)
deno task add:plugin -n org/plugin-name --setup
# Run setup later
deno task setup:plugin -n org/plugin-name
# Update / remove
deno task update:plugin -n org/plugin-name
deno task remove:plugin -n org/plugin-name --cleanStart with the official thunder-core plugin for authentication, RBAC, security headers, CORS, and user management.
Installing a plugin copies its files and merges its import map but does not run setup automatically. Use
--setuporsetup:pluginto create indexes/seed data.
deno task dev # Development server with hot reload
deno task start # Production server
deno task check # Format and lint
deno task update:core # Update the framework core (overwrites core/)
deno task generate:openapi # Generate an OpenAPI spec
deno task generate:sdk # Generate a TypeScript SDK
deno task generate:app # Generate a dashboard app in public/
deno task add:plugin -n org/plugin-name [--setup[=env,...]]
deno task setup:plugin -n org/plugin-name [--envs=env,...]
deno task update:plugin -n org/plugin-name
deno task remove:plugin -n org/plugin-name [--clean[=env,...]]Because validators are declared with shape(), Thunder can derive artifacts from your routes:
- OpenAPI —
deno task generate:openapiwrites a spec you can serve or share. - TypeScript SDK —
deno task generate:sdkproduces a fully typed client. - Dashboard app —
deno task generate:appscaffolds a UI from the thunder-ui boilerplate.
Thunder is stateless and serverless-friendly. The same code runs on serverless platforms or a long-running server via deno task start. For background/async work (which serverless does not support), run a separate worker process or use an external queue — see the full documentation for patterns.
The canonical, always-up-to-date reference for conventions and APIs lives in llms.txt (and llms-full.txt for AI agents, which also pulls in plugin extensions). It is the single source of truth for routing, hooks, validation, models, utilities, and code style.
Contributions are welcome! Please read the Contribution Guide before opening an issue or pull request. It covers the development setup, commit conventions, and the review process.
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold it.
Released under the MIT License. © 2026 Saif Ali Khan.