Hono: Setting up the development environment

Somebody who likes to code
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.
What is Hono?
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.
Why Hono Is a Good Alternative Today?
Edge-Native Design
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.
Web Standards Compliance
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.
Developer Experience
Developers coming from Express will find Hono intuitive. Its route handling and middleware system are similar, but optimized for modern runtimes.
Built-in TypeScript and Middleware Ecosystem
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.
Prerequisites
Project Initialization
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
TypeScript
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 fromrootDiris preserved in theoutDir. 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 thetsconfig.jsonfile. Determines what files to compile.exclude: Specifies which should be skipped when resolvinginclude. The remaining files are compiled. It's also resolved relative to the directory containingtsconfig.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 specialESNextvalue refers to the highest version of TypeScript that our version supports.module: Specifies which module system to use for organizing code (imports/exports).NodeNexttells 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.tsfiles innode_modules).types: By default, all visible@typespackages 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 (.jsxoutput).react: Transform toReact.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.jsfiles 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 acasein a switch statement falls through to the next case without abreak,return, orthrow.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 notationobj['key']instead of dot notationobj.keywhen accessing properties defined by index signatures.noUncheckedIndexedAccess: Makes array/index access return potentiallyundefined, 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",
...
}
...
}
Code Formatter
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: UseCRLFline 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.
Code Analyzer
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 thedistdirectory.
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"
}
}
Environment Variables
To work with environment variables, we will use the following packages:
Dotenv: Reads
.envfiles and loads them intoprocess.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
.envfile:config()loads variables from the.envfile.expand()resolves variable references like${OTHER_VAR}.
Defines expected environment variables:
NODE_ENVmust be one of:development,production, ortest(defaults todevelopment).PORTmust be a number (coerced from string, defaults to3000).
Validates environment variables:
Checks
process.envagainst 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",
...
}
...
}
Debugging
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 indist, not your TypeScript source."preLaunchTask": "npm: build": Runsnpm run buildbefore 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 ourtsconfig.jsonhas"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.
Git Hooks
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"
...
}
...
}
Commit Messages
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.
Visual Studio Code Optimizations
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: Hidesnode_modulesfrom the sidebar.files.watcherExclude: Stops watching files.typescript.tsserver.maxTsServerMemory: Modifies TS server memorytypescript.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.




