Learn
Get started
Introduction
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/app
├── @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/app
├── @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.
- tinyhttp doesn't have the same settings. All
App
settings are initialized in the constructor. You can see a list of them here. - tinyhttp doesn't put
err
object in middleware if the previous one passed error. Instead, it uses a generic error handler. - tinyhttp doesn't include static server and body parser out of the box. To reduce module size these things were put in separate middleware modules, such as
milliparsec
.
Note that maximum compatibility is in progress so some of the points might change.
Install
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
Application
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.listen(3000)
App options can be set inside a constructor.
const app = new App({
noMatchHandler: (req, res) => res.send('Oopsie, page cannot be found')
})
Middleware
Middleware is a an object containing a handler function and a path (optionally), just like in Express.
app
.use((req, _res, next) => {
console.log(`Made a request from ${req.url}!`)
next()
})
.use((_req, res) => res.send('Hello World'))
Handler
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.
Path
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
.
Chaining
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
res.send(file)
})
Routing
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) {
res.send('root')
})
This route path will match requests to /about.
app.get('/about', function (req, res) {
res.send('about')
})
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:
app.get(
'/example/b',
(req, res, next) => {
console.log('the response will be sent by the next function ...')
next()
},
(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!')
next()
}
const cb1 = (req, res, next) => {
console.log('Callback two!')
next()
}
const cb2 = (req, res) => res.send('Hello from Callback three!')
app.get('/example/c', cb0, cb1, cb2)
Subapps
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) => {
console.log(err)
res.status(500).send(`Something bad happened`)
}
})
app
.get('/', async (_, res, next) => {
const file = await readFile(`non_existent_file`)
res.send(file.toString())
})
.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
- sirv - An optimized middleware & CLI application for serving static files
- serve-handler - The foundation of
serve
- serve-static - Serve static files
Template engine
- Eta - Embedded JS template engine for Node, Deno, and the browser. Lighweight, fast, and pluggable. Written in TypeScript
- express-handlebars - A Handlebars view engine for Express which doesn't suck
- EJS - Embedded JavaScript templates for node
Logger
- @tinyhttp/logger - Simple HTTP logger for tinyhttp
- Pino - super fast, all natural json logger
- Zoya - Truly highly composable logging utility
- Morgan - HTTP request logger middleware for Node.js
- concurrency-logger - Log HTTP requests/responses separately, visualize their concurrency, and report logs/errors in the context of a request
- Volleyball - Tiny HTTP logger for Express showing asynchronous requests and responses
Session
- express-session - Simple session middleware for Express
- micro-session - Session middleware for micro
- next-session - Simple promise-based session middleware for Next.js, micro, Express, and more
- node-client-sessions - secure sessions stored in cookies
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.
Example
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'
dotenv.config()
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')
})
app
.get('/notes', async (_, res, next) => {
const r = await coll.find({}).toArray()
res.send(r)
next()
})
.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`)
})
.listen(3000)
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.
Deployment
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.
Serverless
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.
Self-hosted
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.
Tool |
---|
Exoframe |
Custom
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
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
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.