DEV Community

Cover image for Comparing Hattip vs. Express.js for modern app development
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Comparing Hattip vs. Express.js for modern app development

Written by Antonello Zanini✏️

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!

The main issue with Express.js

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!

What is Hattip?

Hattip is a set of JavaScript packages for building HTTP server applications. This technology can be defined as:

  • Modern: Based on current and future web standards, such as the Fetch API
  • Universal: Runs anywhere, including Node.js, Deno, Bun, and Edge runtimes
  • Modular: Allows you to install only the subset of packages you really need
  • Minimalist: Provides only what you need, nothing more, nothing less

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.

Modules offered by Hattip

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>
Enter fullscreen mode Exit fullscreen mode

General packages

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.

Platform adapters

Packages that enable Hattip to run on any platform:

Bundlers

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:

Utilities and middleware

These are useful packages to speed up and simplify development:

  • compose: A middleware system for combining multiple Hattip handlers into a single one
  • router: To write Express-style imperative routers
  • response: Utility functions for producing text, JSON, and HTML server responses
  • headers: Middleware to parse header values
  • multipart: Experimental middleware for multipart parsing
  • cookie: Cookie handling middleware
  • cors: Middleware to handle CORS
  • graphql: GraphQL middleware that wraps the GraphQL Yoga library
  • session: Session middleware to persist data between requests in a cookie or a custom session store

How to use Hattip in Node.js

Follow 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.

Initialize a Hattip project

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
Enter fullscreen mode Exit fullscreen mode

Next, launch the command below to initialize a new npm project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Define some request handlers

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: "jane.smith@example.com", name: "Jane Smith" },
  { id: 2, email: "alice.jones@example.com", name: "Alice Jones" },
  { id: 3, email: "john.doe@example.com", name: "John Doe" },
  { id: 4, email: "bob.miller@example.com", name: "Bob Miller" },
  { id: 5, email: "sara.white@example.com", 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 });
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: "jane.smith@example.com", name: "Jane Smith" },
  { id: 2, email: "alice.jones@example.com", name: "Alice Jones" },
  { id: 3, email: "john.doe@example.com", name: "John Doe" },
  { id: 4, email: "bob.miller@example.com", name: "Bob Miller" },
  { id: 5, email: "sara.white@example.com", 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
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. By not returning anything
  2. By calling the next() method, which allows the handler to modify the response before returning

For 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
Enter fullscreen mode Exit fullscreen mode

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: "jane.smith@example.com", name: "Jane Smith" },
  { id: 2, email: "alice.jones@example.com", name: "Alice Jones" },
  { id: 3, email: "john.doe@example.com", name: "John Doe" },
  { id: 4, email: "bob.miller@example.com", name: "Bob Miller" },
  { id: 5, email: "sara.white@example.com", 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();
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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.

Putting it all together

Add the following entry in the scripts section of your package.json file:

"start": "node src/entry-node.js", 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Execute that command, and you will see the following in the terminal:

Node.js server is listening on http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Note that on Windows, you should use curl.exe instead of curl. Testing this endpoint will return the following:

"Hello, World!"
Enter fullscreen mode Exit fullscreen mode

Similarly, /api/v1/users would produce the following:

{"users":[{"id":1,"email":"jane.smith@example.com","name":"Jane Smith"},{"id":2,"email":"alice.jones@example.com","name":"Alice Jones"},{"id":3,"email":"john.doe@example.com","name":"John Doe"},{"id":4,"email":"bob.miller@example.com","name":"Bob Miller"},{"id":5,"email":"sara.white@example.com","name":"Sara White"}]}
Enter fullscreen mode Exit fullscreen mode

Et voilà! You just learn how to build a Hattip backend application.

Hattip vs. Express.js: Which should you use?

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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.

Conclusion

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!


200s only✓ Monitor failed and slow network requests in production

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 Signup

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.

Top comments (0)