-
Notifications
You must be signed in to change notification settings - Fork 339
docs: add From Express.js to h3 migration guide #1412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thisismairaj
wants to merge
2
commits into
h3js:main
Choose a base branch
from
thisismairaj:docs/from-expressjs-to-h3-1240
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+331
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,330 @@ | ||
| --- | ||
| icon: ph:arrow-right | ||
| --- | ||
|
|
||
| # From Express.js to h3 | ||
|
|
||
| > A practical migration guide for Express.js developers moving to h3 v2. | ||
|
|
||
| h3 has a familiar routing API for Express developers, but is built on web standards, requires no middleware for common tasks, and is runtime-agnostic (Node.js, Bun, Deno, edge runtimes). | ||
|
|
||
| ## Hello World | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| import express from "express"; | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.get("/", (req, res) => { | ||
| res.send("Hello World!"); | ||
| }); | ||
|
|
||
| app.listen(3000); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app.get("/", () => "Hello World!"); | ||
|
|
||
| serve(app, { port: 3000 }); | ||
| ``` | ||
|
|
||
| Key differences: | ||
|
|
||
| - `new H3()` replaces `express()` | ||
| - Handlers **return** the response body instead of calling `res.send()` | ||
| - `serve(app)` starts the server (powered by [srvx](https://srvx.h3.dev/)) | ||
|
|
||
| ## Routing | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| const app = express(); | ||
|
|
||
| app.get("/users", (req, res) => res.json({ users: [] })); | ||
| app.post("/users", (req, res) => res.json({ created: true })); | ||
| app.delete("/users/:id", (req, res) => res.json({ deleted: req.params.id })); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, getRouterParam } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app | ||
| .get("/users", () => ({ users: [] })) | ||
| .post("/users", () => ({ created: true })) | ||
| .delete("/users/:id", (event) => ({ deleted: getRouterParam(event, "id") })); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| h3 returns JSON automatically when you return a plain object. | ||
|
|
||
| ## URL Params | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| app.get("/users/:id", (req, res) => { | ||
| const id = req.params.id; | ||
| res.send(`User: ${id}`); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, getRouterParam, getRouterParams } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| // Single param | ||
| app.get("/users/:id", (event) => { | ||
| const id = getRouterParam(event, "id"); | ||
| return `User: ${id}`; | ||
| }); | ||
|
|
||
| // All params at once | ||
| app.get("/posts/:category/:slug", (event) => { | ||
| const { category, slug } = getRouterParams(event); | ||
| return `${category}/${slug}`; | ||
| }); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| For validated params, use `getValidatedRouterParams` with any [Standard Schema](https://github.com/standard-schema/standard-schema) compatible library (Zod, Valibot, ArkType): | ||
|
|
||
| ```js | ||
| import { H3, serve, getValidatedRouterParams } from "h3"; | ||
| import * as z from "zod"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app.get("/users/:id", async (event) => { | ||
| const params = await getValidatedRouterParams(event, z.object({ | ||
| id: z.string().uuid(), | ||
| })); | ||
| return `User: ${params.id}`; | ||
| }); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| :read-more{to="/examples/validate-data"} | ||
|
|
||
| ## Query String | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| app.get("/search", (req, res) => { | ||
| const q = req.query.q; | ||
| res.send(`Searching for: ${q}`); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, getQuery } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app.get("/search", (event) => { | ||
| const { q } = getQuery(event); | ||
| return `Searching for: ${q}`; | ||
| }); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| ## Request Body | ||
|
|
||
| Express requires `express.json()` middleware. h3 parses the body automatically. | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| const app = express(); | ||
| app.use(express.json()); // required | ||
|
|
||
| app.post("/users", (req, res) => { | ||
| const { name } = req.body; | ||
| res.json({ name }); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, readBody } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app.post("/users", async (event) => { | ||
| const { name } = await readBody(event); | ||
| return { name }; | ||
| }); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| No body-parsing middleware needed — h3 handles JSON, form data, and text automatically. | ||
|
|
||
| ## Cookies | ||
|
|
||
| Express requires the `cookie-parser` package. h3 has built-in cookie utilities. | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| import cookieParser from "cookie-parser"; | ||
|
|
||
| const app = express(); | ||
| app.use(cookieParser()); // required | ||
|
|
||
| app.get("/", (req, res) => { | ||
| const token = req.cookies.token; | ||
| res.cookie("token", "abc123", { httpOnly: true }); | ||
| res.send(`Token was: ${token}`); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, getCookie, setCookie } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app.get("/", (event) => { | ||
| const token = getCookie(event, "token"); | ||
| setCookie(event, "token", "abc123", { httpOnly: true }); | ||
| return `Token was: ${token}`; | ||
| }); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| :read-more{to="/examples/handle-cookie"} | ||
|
|
||
| ## Middleware | ||
|
|
||
| h3 v2 provides lifecycle hooks for request/response middleware instead of a generic `app.use(fn)` middleware chain. | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| app.use((req, res, next) => { | ||
| console.log(`[${req.method}] ${req.path}`); | ||
| next(); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, onRequest, onResponse } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| app | ||
| .use(onRequest((event) => { | ||
| console.log(`[${event.req.method}] ${event.url.pathname}`); | ||
| })) | ||
| .use(onResponse((response, event) => { | ||
| console.log(`[${event.req.method}] ${event.url.pathname} -> ${response.status}`); | ||
| })); | ||
|
|
||
| app.get("/", () => "Hello World!"); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| ## Sub-apps / Nested Routers | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| import express from "express"; | ||
|
|
||
| const apiRouter = express.Router(); | ||
| apiRouter.get("/users", (req, res) => res.json({ users: [] })); | ||
|
|
||
| const app = express(); | ||
| app.use("/api", apiRouter); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve } from "h3"; | ||
|
|
||
| const apiApp = new H3(); | ||
| apiApp.get("/users", () => ({ users: [] })); | ||
|
|
||
| const app = new H3(); | ||
| app.mount("/api", apiApp); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| `app.mount(prefix, subApp)` replaces `app.use(prefix, router)`. Handlers in `apiApp` receive paths relative to `/api`. | ||
|
|
||
| ## Error Handling | ||
|
|
||
| **Express.js** | ||
|
|
||
| ```js | ||
| app.use((err, req, res, next) => { | ||
| res.status(500).json({ error: err.message }); | ||
| }); | ||
| ``` | ||
|
|
||
| **h3** | ||
|
|
||
| ```js | ||
| import { H3, serve, HTTPError, onError } from "h3"; | ||
|
|
||
| const app = new H3(); | ||
|
|
||
| // Throw typed HTTP errors from any handler | ||
| app.get("/protected", () => { | ||
| throw HTTPError.status(401, "Unauthorized"); | ||
| }); | ||
|
|
||
| // Global error hook | ||
| app.use(onError((event, error) => { | ||
| console.error(error.message); | ||
| })); | ||
|
|
||
| serve(app); | ||
| ``` | ||
|
|
||
| h3 automatically sends the correct HTTP status and JSON error body when you throw an `HTTPError`. | ||
|
|
||
| ## What's Not Available in h3 v2 | ||
|
|
||
| | Express feature | h3 v2 equivalent | | ||
| | --- | --- | | ||
| | `express.json()` | Built-in — use `readBody()` | | ||
| | `cookie-parser` | Built-in — use `getCookie()` / `setCookie()` | | ||
| | `express.static()` | Built-in — use `serveStatic()` | | ||
| | `res.redirect()` | `return redirect(event, url)` | | ||
| | `fromNodeMiddleware()` | Not available in v2 (web-standards based) | | ||
| | `createApp()` / `createRouter()` | Replaced by `new H3()` | | ||
|
|
||
| > [!NOTE] | ||
| > h3 v2 is built on web standard primitives and is no longer tied to Node.js-specific APIs. Express middleware that relies on `req`/`res` Node.js objects cannot be used directly. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.