Hono: Setting up the development environment

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
Axios is one of the most popular HTTP client libraries in the JavaScript ecosystem. While the native Fetch API has become more powerful, Axios continues to offer a rich feature set that simplifies common HTTP tasks. One of its most powerful features ...
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

On this page
In the rapidly evolving backend ecosystem, developers are constantly searching for frameworks that balance performance, simplicity, and modern developer experience. While established frameworks like Express and Fastify dominate the Node.js landscape, a new contender, Hono, is quickly gaining attention for its minimalistic design and exceptional performance across multiple runtimes.
This article provides a technical overview of Hono, its architectural principles, and provides a step-by-step guide to get started with it.
Hono is a modern, lightweight web framework designed for building high-performance APIs and web applications across multiple JavaScript runtimes. Created by Yusuke Wada in 2021, Hono (炎 - meaning "flame" in Japanese) has rapidly gained traction in the TypeScript ecosystem due to its exceptional speed, minimal footprint, and runtime-agnostic design.
Unlike traditional Node.js frameworks, Hono is runtime-agnostic. It runs seamlessly on:
and any other environment supporting the Fetch API standard.
This flexibility makes Hono an excellent choice for edge computing, serverless architectures, and high-throughput APIs where low latency and cold start performance are critical. At its core, Hono emphasizes three main principles:
Speed: Built for performance; minimal overhead compared to Express.
Simplicity: Small, readable, and predictable API inspired by frameworks like Express and Koa.
Type Safety: Full TypeScript support with static typing and schema validation tools.
Hono was built with edge runtimes in mind. Unlike Express, which assumes a long-running Node.js process, Hono applications can deploy directly to Cloudflare Workers or Vercel Functions without any adjustments.
Hono relies on Web Standards APIs, which means:
Code written in Hono is more portable.
Developers learn transferable skills rather than framework-specific APIs.
Future runtime migrations require minimal refactoring.
The framework benefits from ongoing standards improvements.
Developers coming from Express will find Hono intuitive. Its route handling and middleware system are similar, but optimized for modern runtimes.
Hono provides excellent TypeScript support out of the box, making type-safe development natural. It also offers an expanding ecosystem of official and third-party middleware.
For simplicity, we will use Node.js as the runtime and npm as the package manager. Run the following commands:
npm create hono@latest node-hono-api -- --template nodejs --install --pm npm
cd node-hono-api
Hono uses TypeScript and includes a default tsconfig.json file, which we will update as follows:
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"strict": true,
"sourceMap": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"types": ["node"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false,
"outDir": "./dist",
"noErrorTruncation": true,
"noPropertyAccessFromIndexSignature": true,
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["**/node_modules", "**/.*/", "dist"],
"include": ["src/**/*", "tests/**/*"]
}
Let's dive into all the options:
rootDir: Specifies the root directory of our input files. TypeScript uses this to control the output directory structure. When files are compiled, their relative path from rootDir is preserved in the outDir. The default value is the longest common path of all non-declaration input files. Determines how the output structure is organized.
include: Specifies which files TypeScript should compile. It's an array of glob patterns that tells the compiler which source files to process. These filenames are resolved relative to the directory containing the tsconfig.json file. Determines what files to compile.
exclude: Specifies which should be skipped when resolving include. The remaining files are compiled. It's also resolved relative to the directory containing tsconfig.json.
outDir: Specifies the output directory where TypeScript places all compiled JavaScript files.
target: Specifies which version of JavaScript the TypeScript compiler should output. The special ESNext value refers to the highest version of TypeScript that our version supports.
module: Specifies which module system to use for organizing code (imports/exports). NodeNext tells TypeScript to use Node.js's native module resolution,
strict: Enables all strict type-checking options at once.
verbatimModuleSyntax: Requires us to use exact import/export syntax that matches our module system. Forces us to be explicit about type-only imports.
skipLibCheck: Skips type checking of declaration files (.d.ts files in node_modules).
types: By default, all visible @types packages are included in our compilation. Specifies which type definition packages to include from @types, preventing unnecessary type definitions from being loaded.
jsx: Specifies how JSX syntax should be transformed.
preserve: Keep JSX as-is (.jsx output).
react: Transform to React.createElement() calls (classic).
react-jsx: Modern transform (automatic runtime, no need to import React).
react-jsxdev: Development version with debugging info.
react-native - Preserves JSX syntax but outputs .js files instead of .jsx.
jsxImportSource: Specifies which package provides the JSX runtime.
sourceMap: Generates source map files (.js.map). These files allow debuggers and other tools to display the original TypeScript source code when actually working with the emitted JavaScript files.
forceConsistentCasingInFileNames: Enforces that file names in imports match the actual casing on disk.
resolveJsonModule: Allows us to import JSON files directly as modules with full type safety.
esModuleInterop: Allows default imports from CommonJS modules.
noUnusedParameters: Reports an error when function parameters are declared but never used. Parameters declaration with names starting with an underscore (_) are exempt from the unused parameter checking.
noUnusedLocals: Reports an error when local variables are declared but never used.
noFallthroughCasesInSwitch: Reports an error when a case in a switch statement falls through to the next case without a break, return, or throw.
allowUnreachableCode: Controls whether TypeScript reports errors for code that can never be executed.
noErrorTruncation: Prevents TypeScript from truncating long error messages.
noPropertyAccessFromIndexSignature: Forces you to use bracket notation obj['key'] instead of dot notation obj.key when accessing properties defined by index signatures.
noUncheckedIndexedAccess: Makes array/index access return potentially undefined, forcing you to handle missing values.
paths: Defines custom module path mappings for cleaner imports, like creating aliases for directories.
We are not explicitly defining the rootDir. Depending on which files we process, TypeScript will automatically set it up. For example:
Input files from include patterns:
├── src/index.ts
├── src/routes/api.ts
├── src/utils/helper.ts
├── tests/api.test.ts
├── tests/unit/user.test.ts
└── tests/integration/db.test.ts
The calculated rootDir will be ..
Input files from include pattern:
├── src/index.ts
├── src/routes/api.ts
└── src/utils/helper.ts
Calculated rootDir will be ./src.
TypeScript's compiler (tsc) converts our TypeScript code to JavaScript but doesn't handle path aliases in the output, which can lead to runtime errors. tsc-alias is a tool that resolves TypeScript path aliases in our compiled JavaScript output.
npm install --save-dev tsc-alias
Modify the default build script like this:
{
...
"scripts": {
...
"build": "tsc && tsc-alias",
...
}
...
}
We will use Prettier, a code formatter that keeps our project's style consistent. Run the following command to install it as a development dependency:
npm install --save-dev prettier
Create a prettier.config.ts file in the root of the project:
import { type Config } from "prettier";
const config: Config = {
trailingComma: "es5",
singleQuote: true,
arrowParens: "avoid",
endOfLine: "crlf",
};
export default config;
trailingComma: Add trailing commas where valid in ES5.
singleQuote: Use single quotes (') instead of double quotes (") for strings.
arrowParens: Omit parentheses when possible for single-parameter arrow functions.
endOfLine: Use CRLF line endings (for Windows users only).
TypeScript support requires Node.js>=22.6.0, and
--experimental-strip-typesis required before Node.js v24.3.0.
Create a .prettierignore file to exclude some files:
dist/
package-lock.json
By default, prettier ignores files in version control systems directories (".git", ".jj", ".sl", ".svn", and ".hg") and
node_modules(unless the--with-node-modulesCLI option is specified).
Add the following scripts to the package.json file:
{
...
"scripts": {
...
"format": "prettier --write .",
"format:check": "prettier --check ."
...
}
...
}
install the Prettier - Code formatter extension for a better development experience. Create a .vscode/settings.json file in the project root:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
This configuration sets Prettier as the default formatter and automatically formats the code on save.
ESLint is a static code analyzer that helps us identify problems in our code. Run the following command to install it:
npm install --save-dev jiti
npm install --save-dev eslint-config-prettier
npm init @eslint/config@latest
For Deno and Bun, TypeScript configuration files are natively supported; for Node.js, we must install the optional dev dependency
jiti.
Follow the prompts to set up ESLint according to our preferences. In our case, we choose:
√ What do you want to lint? · javascript
√ How would you like to use ESLint? · problems
√ What type of modules does your project use? · esm
√ Which framework does your project use? · none
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ Which language do you want your configuration file be written in? · ts
i The config that you've selected requires the following dependencies:
eslint, @eslint/js, globals, typescript-eslint
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
This will create an eslint.config.ts file with a basic configuration that we will replace as follows:
import jseslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
import prettierConfig from 'eslint-config-prettier';
export default defineConfig(
globalIgnores(["dist/**/*"]),
jseslint.configs.recommended,
tseslint.configs.recommended,
prettierConfig
);
jseslint.configs.recommended: Enables recommended JavaScript linting rules.
tseslint.configs.recommended: Enables recommended TypeScript linting rules.
prettierConfig: Disables all previous ESLint formatting rules that conflict with Prettier.
globalIgnores: Ignores all files in the dist directory.
Add the following scripts to the package.json file:
{
...
"scripts": {
...
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:format": "npm run lint:fix && npm run format"
...
}
...
}
Install the ESLint extension for a better development experience. Update the .vscode/settings.json file with the following content:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
To work with environment variables, we will use the following packages:
Dotenv: Reads .env files and loads them into process.env.
Dotenv-expand: Allows referencing other environment variables within .env.
Cross-env: Sets environment variables that work on all operating systems.
Zod: Generates TypeScript types from schemas and validates data at runtime, not just at compile time.
npm install dotenv dotenv-expand cross-env zod
Create a .env file in the project root:
PORT=5000
Create the /src/env.ts file with the following content:
import { config } from "dotenv";
import { expand } from "dotenv-expand";
import { ZodError, z } from "zod";
const ENVSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
PORT: z.coerce.number().default(3000),
});
expand(config());
try {
ENVSchema.parse(process.env);
} catch (error) {
if (error instanceof ZodError) {
const e = new Error(
`Environment validation failed:\n ${z.treeifyError(error)}`,
);
e.stack = "";
throw e;
} else {
console.error("Unexpected error during environment validation:", error);
throw error;
}
}
export const ENV = ENVSchema.parse(process.env);
This file validates and loads environment variables using Zod for type safety. What does it do?
Loads .env file:
config() loads variables from the .env file.
expand() resolves variable references like ${OTHER_VAR}.
Defines expected environment variables:
NODE_ENV must be one of: development, production, or test (defaults to development).
PORT must be a number (coerced from string, defaults to 3000).
Validates environment variables:
Checks process.env against the schema.
Throws a readable error if validation fails.
This pattern ensures our app fails fast with clear errors if the environment configuration is wrong. Replace the index.ts file with the following content:
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { ENV } from "@/env.js";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
serve(
{
fetch: app.fetch,
port: ENV.PORT,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
},
);
Modify the following scripts in the package.json file:
{
...
"scripts": {
...
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
...
"start": "cross-env NODE_ENV=development node dist/index.js",
...
}
...
}
Create a .vscode/launch.json file with the following content:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug TypeScript",
"program": "${workspaceFolder}/dist/index.js",
"preLaunchTask": "npm: build",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true
}
]
}
This is a VS Code debugger configuration for debugging our TypeScript Node.js application.
"type": "node": Specifies this is a Node.js application debugger.
"request": "launch": Launches a new Node.js process.
"name": "Debug TypeScript": The name that appears in VS Code's debug dropdown.
"program": "${workspaceFolder}/dist/index.js": The entry point file to run. Points to compiled JavaScript in dist, not your TypeScript source.
"preLaunchTask": "npm: build": Runs npm run build before debugging starts. Ensures TypeScript is compiled to JavaScript first.
"outFiles": ["${workspaceFolder}/dist/**/*.js"]: Tells debugger where compiled JavaScript files are located. Used for mapping breakpoints
"sourceMaps": true: Enables source map support. Let's us debug TypeScript code instead of compiled JavaScript. Works because our tsconfig.json has "sourceMap": true.
Press F5 or click Debug TypeScript in the Run and Debug panel.
Ensure the program property matches our app's entry point. With our current TypeScript setup, when a test is implemented, the new value must be changed to
${workspaceFolder}/dist/src/index.js.
Husky allows you to run scripts before commits and pushes via git hooks, ensuring code quality standards are maintained automatically. Install it by running the following command:
npm install --save-dev husky
Initialize Husky:
npx husky init
The init command simplifies setting up Husky in a project. It creates a
pre-commitscript in.husky/and updates the prepare script inpackage.json.
Edit the .husky/pre-commit file to run Prettier and ESLint:
npm run lint
npm run format:check
Husky will modify the package.json file by adding the following script:
{
...
"scripts": {
...
"prepare": "husky"
...
}
...
}
Commitlint is a tool that validates commit messages to ensure they follow a consistent format and conventional standards. Install it by running the following command:
npm install --save-dev @commitlint/config-conventional @commitlint/cli @commitlint/prompt-cli
Create the commitlint.config.ts file in the project root:
import type { UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
};
export default Configuration;
This configuration sets up commit message linting for our project. The @commitlint/config-conventional package implements the Conventional Commits specification. The commit message should be structured as follows:
type(scope?): subject
body?
footer?
Create the .husky/commit-msg file with the following content:
npx --no-install commitlint --edit $1
The @commitlint/prompt-cli package helps us create commit messages that follow the commit convention set in the commitlint.config.js file. To make prompt-cli easy to use, add a new script to the package.json file:
{
...
"scripts": {
...
"commit": "commit"
...
}
...
}
"prepare" is a special npm lifecycle script that runs automatically at specific times:
After npm install (when someone clones our repo and installs dependencies).
Before npm publish (when publishing a package).
In our project, we will run the husky command to set up Git hooks in the .husky folder.
Open the .vscode/settings.json file and update the content as follows:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/.git": true
},
"files.exclude": {
"**/node_modules": true
},
"files.watcherExclude": {
"**/node_modules/**": true,
"**/dist/**": true,
"**/.git/**": true
},
"typescript.tsserver.maxTsServerMemory": 4096,
"typescript.tsserver.enableTracing": false,
"editor.minimap.enabled": false
}
search.exclude: Excludes irrelevant directories from search results.
files.exclude: Hides node_modules from the sidebar.
files.watcherExclude: Stops watching files.
typescript.tsserver.maxTsServerMemory: Modifies TS server memory
typescript.tsserver.enableTracing: Reduces overhead from TypeScript debugging.
editor.minimap.enabled: Removes the code minimap for more space.
Now we are ready to start working on our app. In future articles, we will review many Hono features, so stay tuned. You can find all the code here. Thanks, and happy coding.