Hattip aims to define an ecosystem of universal middlewares to build HTTP server apps that can run on any JavaScript platform. Instead of building backend applications tailored for Express.js, this collection of packages allows you to write server code that can be deployed anywhere, including AWS, Fastly, Vercel, or a custom VPS.
With its first commit on Feb 22, 2022, Hattip is a relatively new technology. In this article, you will learn what Hattip is, what it has to offer, how it works, and how to use it to build a Node.js HTTP backend server. At the end of this guide, we’ll explore the differences between Hattip and Express.js.
Let’s dive in!
Express.js is a minimalist and versatile web framework for creating scalable and robust backend applications in Node.js. Over time, it has become so popular that the term “Node.js” is commonly used as a synonym for “Express.js.”
Now, suppose you had an Express.js backend that exposes some API endpoints. If you wanted to deploy it, you would need to set up a Node.js environment and launch the application inside it.
But what if you wanted to run the same application on a Deno server or a specific edge runtime such as Cloudflare Workers or Vercel? That would require changes in your codebase, which is tedious, time-consuming, and error-prone.
This is a problem! Ideally, your backend application should be platform-independent by following the standardized Web API. This way, you could run it across most JavaScript platforms.
That is exactly what Hattip is all about!
Hattip is a set of JavaScript packages for building HTTP server applications. This technology can be defined as:
The ultimate goal of Hattip is to build an ecosystem of middlewares that can be used across the entire JavaScript universe. The motto of the library is:
“Instead of writing server code that only works with Express.js, write server code that can be deployed anywhere: AWS, Cloudflare Workers, Fastly, Vercel, VPS, …”
This project enables you to write HTTP server applications in JavaScript based on a standardized and interoperable API. You can then execute this code on any compatible JavaScript platform or technology thanks to custom adapters.
At the time of this writing, the supported adapters are Bun, Cloudflare Workers, Deno, Express.js, Fastly, Lagon, Netlify Edge Functions, Netlify Functions, Node.js, uWebSockets.js, Vercel Edge, and Vercel Serverless.
The team behind Hattip believes in a heterogeneous but interoperable future for the JavaScript ecosystem. This is why they are closely following WinterCG, a community of people who are interested in using the JavaScript Web APIs on the servers and edge runtimes.
Hattip is modular and consists of many packages. Let’s see them all, broken down by type.
Note that every npm Hattip module starts with the “@hattip” prefix. Thus, you can install any packages below with the following npm command:
npm install @hattip/<module_name>
General packages that you need to keep in mind when developing with Hattip:
core
: A type-only package for TypeScript development that defines the interface between your application and platform adapters.polyfills
: A collection of polyfills used by adapters for compatibility across platforms.Packages that enable Hattip to run on any platform:
adapter-node
adapter-cloudflare-workers
adapter-vercel-edge
adapter-netlify-functions
adapter-netlify-edge
adapter-deno
adapter-bun
adapter-fastly
adapter-lagon
adapter-uwebsockets
Workers and serverless platforms generally require your code to be bundled in a specific form. These packages provide bundlers fine-tuned for their respective platforms:
These are useful packages to speed up and simplify development:
compose
: A middleware system for combining multiple Hattip handlers into a single onerouter
: To write Express-style imperative routersresponse
: Utility functions for producing text, JSON, and HTML server responsesheaders
: Middleware to parse header valuesmultipart
: Experimental middleware for multipart parsingcookie
: Cookie handling middlewarecors
: Middleware to handle CORSgraphql
: GraphQL middleware that wraps the GraphQL Yoga librarysession
: Session middleware to persist data between requests in a cookie or a custom session storeFollow this step-by-step tutorial and learn how to set up a Hattip project for Node.js. Check out the GitHub repository with the code of the Hattip example application you will build here.
As of this writing, initializing a Hattip project requires some manual commands. However, keep in mind that a zero-config development environment based on Vite is in the works.
First, create a folder for your Hattip Node.js project and enter it in the terminal:
mkdir hattip-nodejs-demo cd hattip-nodejs-demo
Next, launch the command below to initialize a new npm project:
npm init -y
This will create a package.json
file in your project’s folder. Add the "type": "module"
option to it so that you can use ESM imports. Find out more in our guide on using ES modules in Node.js.
As mentioned earlier, Hattip is a modular library. This means you can install only the modules you really need. For now, the @hattip/response
module will be enough:
npm install @hattip/response
Then, create a /src
folder and place an empty entry-hattip.js
file inside it. That is the naming convention for the Hattip backend entry file.
Awesome! You now have a Hattip project in place!
A Hattip handler defines one or more API endpoints and works anywhere, such as on Node.js, Cloudflare Workers, Fastly, Vercel, and more.
This is how you can define a sample Hattip handler:
// src/entry-hattip.js import { json } from "@hattip/response"; // local array to simulate a database let users = [ { id: 1, email: "[email protected]", name: "Jane Smith" }, { id: 2, email: "[email protected]", name: "Alice Jones" }, { id: 3, email: "[email protected]", name: "John Doe" }, { id: 4, email: "[email protected]", name: "Bob Miller" }, { id: 5, email: "[email protected]", name: "Sara White" }, ]; export default (context) => { // read the API endpoint const { pathname } = new URL(context.request.url); // read the data from the request const { method, body } = context.request; // define a new GET endpoint if (method === "GET" && pathname === "/api/v1/greetings/hello-world") { return new json("Hello, World!"); } // define another GET endpoint if (method === "GET" && pathname === "/api/v1/users") { return new json({ users: users }); } // other endpoints... // handle all remaining requests return new json({ error: "Endpoint not found!" }, { status: 404 }); };
A Hattip handler is a function that receives a context
object as a parameter. This represents the HTTP request context and contains the standard Request
object in the request
attribute.
json()
is a special function from the @hattip/response
module that creates a standard Response
object with the given JSON object. Both Response
and Request
objects follow the Fetch API standard.
Note that the same JavaScript file can also contain several request handlers. To compose them all into a single Hattip handler, you need to use the compose()
function from the @hattip/compose
module.
Install @hattip/compose
with the following command:
npm install @hattip/compose
Next, use the compose()
function to compose multiple handlers into a single one as shown below:
// src/entry-hattip.js import { compose } from "@hattip/compose"; import { json } from "@hattip/response"; // middleware to parse the URL into a URL object const urlParserMiddleware = (context) => { // extend the context with custom data context.url = new URL(context.request.url); }; const greetingHandler = (context) => { if ( context.request.method === "GET" && context.url.pathname === "/api/v1/greetings/hello-world" ) { return new json("Hello, World!"); } // other endpoints... }; // local array to simulate a database let users = [ { id: 1, email: "[email protected]", name: "Jane Smith" }, { id: 2, email: "[email protected]", name: "Alice Jones" }, { id: 3, email: "[email protected]", name: "John Doe" }, { id: 4, email: "[email protected]", name: "Bob Miller" }, { id: 5, email: "[email protected]", name: "Sara White" }, ]; const userHandler = (context) => { if ( context.request.method === "GET" && context.url.pathname === "/api/v1/users" ) { return new json({ users: users }); } // other endpoints... }; // handle all remaining requests const request404Handler = (context) => { return new json({ error: "Endpoint not found!" }, { status: 404 }); // other endpoints... }; // other handlers... // middleware to add an "X-Powered-By" header // to the response const poweredByMiddleware = async (context) => { // get the current response const response = await context.next(); // add a custom header and // return the response response.headers.set("X-Powered-By", "Hattip"); return response; }; // compose all export default compose( urlParserMiddleware, greetingHandler, userHandler, request404Handler, poweredByMiddleware );
Each handler can define either an API endpoint or a middleware function. When the server receives a request, Hattip calls each handler function in sequence until one returns a response. A handler can pass control to the next handler in two ways:
next()
method, which allows the handler to modify the response before returningFor better code organization, you can also create one file for Hattip handler, import them all in entry-hattip.js
, and compose them with compose()
.
For an Express-like alternative approach to request handler definition, you can use the @hattip/router
package. Install it with this command:
npm install @hattip/router
This module enables you to write request handlers with a syntax that is similar to Express.js routers. For example, this is how you can define a Hattip handler with a router:
// src/entry-hattip.js import { createRouter } from "@hattip/router"; import { json } from "@hattip/response"; // local array to simulate a database let users = [ { id: 1, email: "[email protected]", name: "Jane Smith" }, { id: 2, email: "[email protected]", name: "Alice Jones" }, { id: 3, email: "[email protected]", name: "John Doe" }, { id: 4, email: "[email protected]", name: "Bob Miller" }, { id: 5, email: "[email protected]", name: "Sara White" }, ]; // initialize a Hattip router const router = createRouter(); // register some endpotins... router.get("/api/v1/greetings/hello-world", () => { return new json("Hello, World!"); }); router.get("/api/v1/users", () => { return new json({ users: users }) }); // other endpoints // handle 404 errors router.use(() => { return new json({ error: "Endpoint not found!" }, { status: 404 }); }); // transform the router into a handler and // return it export default router.buildHandler();
Again, you can split the routers into several files and import them all in a single entry-hattip.js
. Just as in Express.js, utilize the use()
function to add a Hattip middleware or handler for all methods and all paths. That is how you can combine many Hattip routers into a single handler.
Great! All that remains is to pass the Hattip handler to a Node.js server.
Install the adapter for the underlying technology you want the Hattip application to operate on. In this case, our target runtime environment Node.js. Thus, you have to install the @hattip/adapter-node
package:
npm install @hattip/adapter-node
Create an entry-node.js
file inside /src
where to initialize your Node.js file:
import { createServer } from "@hattip/adapter-node"; import handler from "./entry-hattip.js"; const HOST = "localhost" const PORT = 3000 createServer(handler).listen(PORT, HOST, () => { console.log(`Node.js server is listening on http://${HOST}:${PORT}`); });
This uses the createServer()
function to transform the Hattip handler imported from entry-http.js
into a Node.js http
server.
You can use this approach with other adapters to make Hattip work with different technologies.
Add the following entry in the scripts
section of your package.json
file:
"start": "node src/entry-node.js",
As you can see, the startup command of the Hattip application is equivalent to that of a traditional Node.js application.
You will now be able to launch your Hattip Node.js application with this command:
npm run start
Execute that command, and you will see the following in the terminal:
Node.js server is listening on http://localhost:3000
Test the GET
/api/v1/greetings/hello-world
endpoint with the curl
command below:
curl -X GET http://localhost:3000/api/v1/greetings/hello-world
Note that on Windows, you should use curl.exe
instead of curl
. Testing this endpoint will return the following:
"Hello, World!"
Similarly, /api/v1/users
would produce the following:
{"users":[{"id":1,"email":"[email protected]","name":"Jane Smith"},{"id":2,"email":"[email protected]","name":"Alice Jones"},{"id":3,"email":"[email protected]","name":"John Doe"},{"id":4,"email":"[email protected]","name":"Bob Miller"},{"id":5,"email":"[email protected]","name":"Sara White"}]}
Et voilĂ ! You just learn how to build a Hattip backend application.
The biggest advantage Hattip offers over Express.js is standardization and interoperability. This is great because it makes your JavaScript backend applications platform-independent and future-ready.
On the other hand, the main reason Express is so popular is its large ecosystem of libraries. What if your project relies on an Express-specific middleware function that has no replacement elsewhere? This may easily prevent you from even starting to translate your code into Hattip.
The Hattip team has thought of that scenario. Thanks to the Express adapter, you can use any existing Express middleware as below:
// src/entry-express.js import { createMiddleware } from "@hattip/adapter-node"; import handler from "./entry-hattip.js"; import express from "express"; // the Express middleware that does not have an equivalent // in Hattip or any other technology supported by Hattip import expressSpecificMiddleware from "<express-specific-middleware>"; // transfrom the Hattip handler into // a Node.js middleware const hattip = createMiddleware(handler); // initialize an Express application const app = express(); // register the Express-specific middleware app.use(expressSpecificMiddleware()); // register the Hattip handler app.use(hattip); const HOST = "localhost" const PORT = 3000 app.listen(PORT, HOST, () => { console.log(`Express server is listening on http://${HOST}:${PORT}`); });
When the community releases an equivalent Hattip middleware, you will no longer need the Express adapter. That means you will be able to run your server code with any other adapter.
Now, does it really make sense to convert your entire Express.js code base to Hattip?
If you already have an Express application, you most likely also have a deployment system. In this case, converting the entire code base to Hattip may not be worth the effort.
However, if you instead need to start a new project, Hattip might be a good alternative to Express.js, especially with an eye toward the future.
In this article, you learned the main issue with Express.js and how to address it with the new JavaScript technology Hattip.
As you saw here, Hattip is a JavaScript set of packages to build platform-independent server applications that rely on an interoperable API. This opens the door to writing server code that can be deployed anywhere, from AWS or Vercel to a custom VPS.
If you want to decouple your backend application from Node.js so that you can deploy it to other servers, then Hattip is the technology you are looking for!
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]