Get started


tinyhttp is a modern Express-like web framework for Node.js written in TypeScript. It uses a bare minimum amount of dependencies trying to avoid legacy hell. Besides a small dependency list, tinyhttp also offers native ESM (import / export) support, async middleware handlers support, and proper types out of the box.

Also, because a lot of express middleware depends on legacy modules themselves, tinyhttp offers a list of tinyhttp-oriented middleware (with types out of the box and native ESM support as well) for the best development experience.

Dependency structure

tinyhttp directly mostly depends on helper modules:

├── @tinyhttp/cookie
├── @tinyhttp/proxy-addr
├── @tinyhttp/req
├── @tinyhttp/res
├── @tinyhttp/router
├── header-range-parser
└── regexparam

The core @tinyhttp/app module depends on small helper modules like @tinyhttp/router for easier maintenance and the ability to use tinyhttp features outside of tinyhttp (to build your own web framework for example).

this is how the full dependency tree of @tinyhttp/app looks like:

├── @tinyhttp/cookie 
├─┬ @tinyhttp/proxy-addr 
 ├── @tinyhttp/forwarded 
 └── ipaddr.js 
├─┬ @tinyhttp/req 
 ├─┬ @tinyhttp/accepts 
  ├─┬ es-mime-types 
   └── mime-db
  └── negotiator 
 ├─┬ @tinyhttp/type-is 
  ├── es-content-type 
  └─┬ es-mime-types 
    └── mime-db
 ├── @tinyhttp/url 
 ├── es-fresh
 └── header-range-parser 
├─┬ @tinyhttp/res 
 ├── @tinyhttp/content-disposition 
 ├── @tinyhttp/cookie 
 ├── @tinyhttp/cookie-signature 
 ├── @tinyhttp/encode-url 
 ├─┬ @tinyhttp/req 
  ├─┬ @tinyhttp/accepts 
   ├─┬ es-mime-types 
    └── mime-db
   └── negotiator 
  ├─┬ @tinyhttp/type-is 
   ├── es-content-type 
   └─┬ es-mime-types 
     └── mime-db
  ├── @tinyhttp/url 
  ├── es-fresh
  └── header-range-parser 
 ├─┬ @tinyhttp/send 
  ├── @tinyhttp/etag 
  ├── es-content-type 
  └─┬ es-mime-types 
    └── mime-db
 ├── es-escape-html 
 ├─┬ es-mime-types 
  └── mime-db
 └── es-vary 
├── @tinyhttp/router 
├── header-range-parser 
└── regexparam 

Because Express contains a lot of legacy modules, the dependency tree of tinyhttp is much smaller.

Differences with Express

Although tinyhttp tries to be as close to Express as possible, there are some key differences between these two frameworks.

Note that maximum compatibility is in progress so some of the points might change.


tinyhttp requires Node.js 14.21.3 or newer or newer. It is recommended to use pnpm because tinyhttp reuses modules in some middlewares, although it's optional.

You can quickly setup a working app with tinyhttp CLI:

# Install tinyhttp CLI
pnpm i -g @tinyhttp/cli

# Create a new project
tinyhttp new basic my-app

# Go to project directory
cd my-app

# Run your app
node app.js

Hello World

Here is a very basic example of a tinyhttp app:

import { App } from '@tinyhttp/app'

const app = new App()

app.use((req, res) => res.send('Hello world!'))

app.listen(3000, () => console.log('Started on http://localhost:3000'))

For more examples check examples folder in tinyhttp repo.

Main concepts


A tinyhttp app is an instance of App class containing middleware and router methods.

import { App } from '@tinyhttp/app'

const app = new App()

app.use((req, res) => res.send('Hello World'))


App options can be set inside a constructor.

const app = new App({
  noMatchHandler: (req, res) => res.send('Oopsie, page cannot be found')


Middleware is a an object containing a handler function and a path (optionally), just like in Express.

  .use((req, _res, next) => {
    console.log(`Made a request from ${req.url}!`)
  .use((_req, res) => res.send('Hello World'))


Handler is a function that accepts Request and Response object as arguments. These objects are extended versions of built-in http's IncomingMessage and ServerResponse.

app.use((req, res) => {
  res.send({ query: req.query })

For a full list of those extensions, check the docs.


the request URL starts with the specified path, the handler will process request and response objects. Middleware only can handle URLs that start with a specified path. For advanced paths (with params and exact match), go to the Routing section.

app.use('/', (_req, _res, next) => void next()) // Will handle all routes
app.use('/path', (_req, _res, next) => void next()) // Will handle routes starting with /path

path argument is optional (and defaults to /), so you can put your handler function as the first argument of app.use.


tinyhttp app returns itself on any app.use call which allows us to do chaining:

app.use((_) => {}).use((_) => {})

Routing functions like app.get support chaining as well.

Execution order

All middleware executes in a loop. Once a middleware handler calls next() tinyhttp goes to the next middleware until the loop finishes.

app.use((_, res) => res.end('Hello World')).use((_, res) => res.end('I am the unreachable middleware'))

Remember to call next() in your middleware chains because otherwise it will stick to a current handler and won't switch to next one.

Async handlers

tinyhttp, unlike Express, supports asynchronous functions as handlers. Any error thrown by a promise will be caught by a top-level try...catch, meaning that you don't have to wrap it in your own try...catch unless you need it.

import { App } from '@tinyhttp/app'
import { readFile } from 'fs/promises'

app.use('/', async (req, res, next) => {
  const file = await readFile('file.txt') // in case of error it will send 500 with error message


Routing is defining how your application handles requests using with specific paths (e.g. / or /test) and methods (GET, POST etc).

Each route can have one or many middlewares in it, which handle when the path matches the request URL.

Routes usually have the following structure:

app.METHOD(path, handler, ...handlers)

Where app is tinyhttp App instance, path is the path that should match the request URL and handler (+ handlers) is a function that is executed when the specified paths match the request URL.

import { App } from '@tinyhttp/app'

const app = new App()

app.get('/', (_req, res) => res.send('Hello World'))

Router functions

Most popular methods (e.g. GET, POST, PUT, OPTIONS) have pre-defined functions for routing. In the future releases of tinyhttp all methods will have their functions.

app.get('/', (_req, res) => res.send('Hello World')).post('/a/b', (req, res) => res.send('Sent a POST request'))

To handle all HTTP methods, use app.all:

app.all('*', (req, res) => res.send(`Made a request on ${req.url} via ${req.method}`))

Route paths

Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expressions (not yet).

tinyhttp uses a regexparam module to do route matching. For more information about routing patterns, check its README.

Note that query strings (symbols after last ? in request URL) are stripped from the path.

Here are some examples of route paths based on strings.

This route path will match requests to the root route, /.

app.get('/', function (req, res) {

This route path will match requests to /about.

app.get('/about', function (req, res) {

This route path will match requests to /random.text.

app.get('/random.text', (req, res) => res.send('random.text'))

This route path will match acd and abcd.

app.get('/ab?cd', (req, res) => res.send('ab?cd'))
Route parameters

Route parameters are named URL segments that are used to capture the values specified at their position in the URL. The captured values are populated in the req.params object, with the name of the route parameter specified in the path as their keys.

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }

To define routes with route parameters, simply specify the route parameters in the path of the route:

app.get('/users/:userId/books/:bookId', (req, res) => res.send(req.params))

Route handlers

You can provide multiple callback functions that behave like middleware to handle a request. The only exception is that these callbacks might invoke next() to bypass the remaining route callbacks. You can use this technique to conditionally switch or skip middleware when it's not required anymore to stay in the current middleware.

Route handlers can be in the form of a function or a list of functions, as shown in the following examples.

A single callback function can handle a route. For example:

app.get('/example/a', (req, res) => res.send('Hello from A!'))

More than one callback function can handle a route (make sure you specify the next function). For example:

  (req, res, next) => {
    console.log('the response will be sent by the next function ...')
  (req, res) => res.send('Hello from B!')

A list of callback functions can handle a route. For example:

const cb0 = (req, res, next) => {
  console.log('Callback one!')

const cb1 = (req, res, next) => {
  console.log('Callback two!')

const cb2 = (req, res) => res.send('Hello from Callback three!')

app.get('/example/c', cb0, cb1, cb2)


You can use tinyhttp's App to create a modular group of handlers and then bind them to another "main" App.

Each app has its own pack of middleware, settings, and locales in it. Currently, the support is experimental and probably will not work as expected (not all cases are tested yet), but you still could try it:

import { App } from '@tinyhttp/app'

const app = new App()

const subApp = new App()

subApp.get('/route', (req, res) => res.send(`Hello from ${subApp.mountpath}`))

app.use('/subapp', subApp).listen(3000)

// localhost:3000/subapp/route will send "Hello from /subapp"

Error handling

Errors in async middlewares are handled by tinyhttp but you can add your own try...catch to handle a specific error. Although, it's recommended to use a centralized onError handler instead.

import { App } from '@tinyhttp/app'
import { readFile } from 'fs/promises'

const app = new App({
  // Custom error handler
  onError: (err, _req, res) => {
    res.status(500).send(`Something bad happened`)

  .get('/', async (_, res, next) => {
    const file = await readFile(`non_existent_file`)
  .listen(3000, () => console.log('Started on http://localhost:3000'))

Template engines

Starting from v0.2.70, tinyhttp had basic support for template engines. Since v2.2, the view/template engine has been revamped for better compatibility with express.

In order to use an engine, you should first register it for a specific extension.

import { App } from '@tinyhttp/app'
import eta from 'eta'

const app = new App()

app.engine('eta', eta.renderFile) // maps app.engines['eta'] to eta.renderFile function

Additionally, you can set a default engine (which will be used for all templates by default):

app.set('view engine', 'eta')

And now it's possible to render any template file using the res.render method:

import { App } from '@tinyhttp/app'
import eta from 'eta'
import type { PartialConfig } from 'eta/dist/types/config' // make `res.render` inherit the template engine settings type

const app = new App()

app.engine('eta', eta.renderFile)

app.use((_, res) => void res.render<PartialConfig>('index.eta', { name: 'Eta' }))

app.listen(3000, () => console.log(`Listening on http://localhost:3000`))

For advanced configuration, refer to custom view and eta examples.

Common tasks

As a rule, when you develop web applications, a web framework is not enough. This section will show some of the options to solve common problems, such as static serving, logging, etc.

Static server

Template engine



Advanced topics

Usage with Deno

tinyhttp has an experimental Deno port.

There are multiple ways to import tinyhttp in Deno:

import { App } from 'https://deno.land/x/tinyhttp/mod.ts' // official registry
import { App } from 'https://x.nest.land/tinyhttp/mod.ts' // nest.land registry
import { App } from 'https://denopkg.com/deno-libs/tinyhttp/mod.ts' // directly from GitHub

// all of them work the same

Right now the port is experiencing a major rewrite and is not usable on latest deno versions.

Database integration

As any other web framework, tinyhttp works well with databases. There is plenty of examples for database integration, including MongoDB, Fauna, Postgres, and others.


You first need to initialize a client for your database. Then you can use it inside middleware to execute queries.

Here's a simple example with MongoDB:

import { App } from '@tinyhttp/app'
import * as dotenv from '@tinyhttp/dotenv'
import { urlencoded as parser } from 'milliparsec'
import mongodb from 'mongodb'
import assert from 'assert'


const app = new App()

let db
let coll

// create mongo client
const client = new mongodb.MongoClient(process.env.DB_URI, {
  useUnifiedTopology: true

// connect to mongodb
client.connect(async (err) => {
  assert.notStrictEqual(null, err)
  console.log('successfully connected to MongoDB')
  db = client.db('notes')
  coll = db.collection('notes')

  .get('/notes', async (_, res, next) => {
    const r = await coll.find({}).toArray()
  .use('/notes', parser())
  .post('/notes', async (req, res, next) => {
    const { title, desc } = req.body
    const r = await coll.insertOne({ title, desc })
    assert.strictEqual(1, r.insertedCount)
    res.send(`Note with title of "${title}" has been added`)

There are some middlewares for databases to be able to use a database through req.db property but currently, they are in progress. You still can use tinyhttp with any database, as shown in the example.


There are a lot of ways to deploy tinyhttp. You can use a serverless platform, a VPS, or anything else that has Node.js runtime. We'll look into the most common ways and break them down.


As for Serverless, you can pick any of the serverless platforms. Here is a table of some popular ones:

Platform Free
Heroku No
Vercel (Lambda) Yes
AWS Yes (one year)
Render Yes
Deta Yes

You can check out the Vercel and AWS examples in the tinyhttp repo.

If you know any other good serverless platforms to deploy tinyhttp on, feel free to PR on the docs.


There is a list of self-hosted serverless deployments tools that you can install on your VPS and use, making it similar to "real" serverless.



If you prefer doing customized deployments you can try to use a combination of a CI/CD service, process manager, and a web server (or only of them).


CI/CD Free
Github Actions Yes
Travis Yes

Any CI will work for tinyhttp because it doesn't set any limits.

Process managers / Unit systems

PM / Unit system Cross-platform Load balancer built-in
PM2 Yes Yes
systemd No No
z1 Yes Yes
Forever Yes Yes

As a rule, the target server runs on Linux. All of the major distros have systemd. You can use it to create a service for your tinyhttp app.

The most popular process manager for Node.js is PM2. It has a clustering feature built-in so it's very easy to make your app multi-process. However, using pm2 is not required to have clustering. You can do the same without it using the built-in cluster module. Check the cluster example for more info.

Web servers

It is common to use a web server as a reverse proxy from the 3000 (or any other) port to an 80 HTTP port. A web server also could be used for load balancing.

Web server Load balancer built-in Docs
nginx Yes Load Balancing Node.js Application Servers with NGINX
Caddy Yes Caddy Reverse Proxy


Docker has a lot of images to run a Node.js app in a container. One of the most popular images is node.

There are articles on deploying an Express / Node.js app with Docker. You can use these tutorials to deploy tinyhttp.