Cpeak is a minimal and fast Node.js framework inspired by Express.js & Fastify.
This project is designed to be improved until it's ready for use in complex production applications, aiming to be more performant and have a cleaner structure than Express.js and other common frameworks.
Cpeak replaces Express.js, body-parser, cookie-parser, compression, CORS, Passport and more, in one zero-dependency package. You can use it as a drop-in replacement for Express.js, and many npm packages that work with Express.js will also work with Cpeak.
Cpeak currently has almost all the features of Express.js, with roughly the same performance as Fastify. Additionally, it has a codebase that you can easily navigate, audit and change if you want. Benchmarks and test results, and videos on navigating and understanding the codebase will be added soon.
While Cpeak is being built for production, it is also an educational project that was started as part of the Understanding Node.js: Core Concepts course. If you want to learn how to build a framework like this, and get to a point where you can build things like this yourself and beyond, check out this course!
Ready to dive in? Install Cpeak via npm:
npm install cpeak
Cpeak is a pure ESM package, and to use it, your project needs to be an ESM as well. You can learn more about that here.
import cpeak from "cpeak";
const server = cpeak();
server.route("get", "/", (req, res) => {
return res.json({ message: "Hi there!" });
});
server.listen(3000, () => {
console.log("Server has started on port 3000");
});
Include the framework like this:
import cpeak from "cpeak";
Because of the minimalistic philosophy, you won’t add unnecessary objects to your memory as soon as you include the framework. If at any point you want to use a particular utility function (like parseJSON and serveStatic), include it like the line below, and only at that point will it be moved into memory:
import cpeak, { serveStatic, parseJSON } from "cpeak";
Initialize the Cpeak server like this:
const server = cpeak();
Now you can use this server object to start listening, add route logic, add middleware functions, and handle errors.
If you add a middleware function, that function will run before your route logic kicks in. Here you can customize the request object, return an error, or do anything else you want to do prior to your route logic, like authentication.
After calling next, the next middleware function is going to run if there’s any; otherwise, the route logic is going to run.
server.beforeEach((req, res, next) => {
if (req.headers.authentication) {
// Your authentication logic...
req.userId = "<something>";
req.custom = "This is some string";
next();
} else {
// Return an error and close the request...
return res.status(401).json({ error: "Unauthorized" });
}
});
server.beforeEach((req, res, next) => {
console.log(
"The custom value was added from the previous middleware: ",
req.custom
);
next();
});
You can also add middleware functions for a particular route handler like this:
const requireAuth = (req, res, next) => {
// Check if user is logged in, if so then:
req.test = "this is a test value";
next();
// If user is not logged in:
throw { status: 401, message: "Unauthorized" };
};
server.route("get", "/profile", requireAuth, (req, res) => {
console.log(req.test); // this is a test value
});
You can add as many middleware functions as you want for a route:
server.route(
"get",
"/profile",
requireAuth,
anotherFunction,
oneMore,
(req, res) => {
// your logic
}
);
You can add new routes like this:
server.route("patch", "/the-path-you-want", (req, res) => {
// your route logic
});
First add the HTTP method name you want to handle, then the path, and finally, the callback. The req and res object types are the same as in the Node.js HTTP module (http.IncomingMessage and http.ServerResponse). You can read more about them in the official Node.js documentation.
Under the hood, Cpeak stores your routes in a radix tree, so finding the right route for an incoming request is roughly O(log n) in your total route count. In plain terms, that means matching stays fast even when your app has hundreds or thousands of routes. You won't pay a linear scan per request as your route table grows.
If no route or middleware matches an incoming request, Cpeak returns a default 404 message. You can replace it with your own handler using server.fallback:
server.fallback((req, res) => {
return res.status(404).json({ error: "not found" });
});
Use this for custom 404 pages, JSON error envelopes, or serving an SPA's index.html for unknown paths. Static, param, and * wildcard routes still win when they match, the fallback only fires once the router has no match.
To be more consistent with the broader Node.js community and frameworks, we call the HTTP URL parameters (query strings) 'query', and the path variables (route parameters) 'params'.
Here’s how we can read both:
// Imagine request URL is example.com/test/my-title/more-text?filter=newest
server.route("patch", "/test/:title/more-text", (req, res) => {
const title = req.params.title;
const filter = req.query.filter;
console.log(title); // my-title
console.log(filter); // newest
});
You can send a file as a Node.js Stream anywhere in your route or middleware logic like this:
server.route("get", "/testing", (req, res) => {
return res.status(200).sendFile("<file-path>", "<mime-type>");
// Example:
// return res.status(200).sendFile("./images/sun.jpeg", "image/jpeg");
});
The file’s binary content will be in the HTTP response body content. Make sure you specify a correct path relative to your CWD (use the path module for better compatibility).
The MIME type argument is optional. When omitted, Cpeak infers it from the file extension using its built-in MIME registry (see MIME Types):
return res.status(200).sendFile("./images/sun.jpeg");
Passing the MIME type explicitly is the fastest path, Cpeak skips the extension lookup entirely.
If you want to redirect to a new URL, you can simply do:
res.redirect("https://whatever.com");
serveStatic, res.sendFile, and res.render all share a single MIME type registry. Out of the box it covers the common web types:
html: "text/html",
css: "text/css",
js: "application/javascript",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
gif: "image/gif",
ico: "image/x-icon",
txt: "text/plain",
json: "application/json",
webmanifest: "application/manifest+json",
eot: "application/vnd.ms-fontobject",
otf: "font/otf",
ttf: "font/ttf",
woff: "font/woff",
woff2: "font/woff2"
If you need to serve something else, register it once on the cpeak() constructor:
const server = cpeak({
mimeTypes: { mp3: "audio/mpeg", m4a: "audio/mp4" }
});
We keep this list small on purpose. Cpeak isn't going to load the thousands of MIME types from the IANA registry into your process memory on the off chance you might serve an .apk or a .fla one day. You tell us what you serve, we keep memory tight.
You can enable HTTP response compression at construction time. Once enabled, serveStatic, res.json() and res.sendFile() will compress eligible responses automatically, and you also get a res.compress() method on the response for custom payloads.
Fire it up with the defaults like this:
const server = cpeak({ compression: true });
Or pass options to tune the behavior:
const server = cpeak({
compression: {
threshold: 1024, // bytes — responses smaller than this are sent uncompressed. Default: 1024
brotli: {}, // node:zlib BrotliOptions
gzip: {}, // node:zlib ZlibOptions
deflate: {} // node:zlib ZlibOptions
}
});
For arbitrary payloads, like a Buffer, string, or Readable stream, use res.compress:
server.route("get", "/report", async (req, res) => {
const csv = await buildCsvReport();
await res.compress("text/csv", csv);
});
When you're streaming, you can pass a known size as the third argument. Cpeak will use it to decide eligibility against threshold, and to set Content-Length if the body ends up being sent uncompressed:
import { Readable } from "node:stream";
server.route("get", "/proxy/feed", async (req, res) => {
const upstream = await fetch("https://example.com/feed.xml");
const size = Number(upstream.headers.get("content-length"));
await res.compress("application/xml", Readable.fromWeb(upstream.body), size);
});
You must first enable compression at construction time to use res.compress.
If anywhere in your route functions or route middleware functions you want to return an error, you can just throw the error and let the automatic error handler catch it:
server.route("get", "/api/document/:title", (req, res) => {
const title = req.params.title;
if (title.length > 500) throw { status: 400, message: "Title too long." };
// The rest of your logic...
});
Make sure to call server.handleErr and pass a function like this to have the automatic error handler work properly:
server.handleErr((error, req, res) => {
if (error && error.status) {
return res.status(error.status).json({ error: error.message });
} else {
// Log the unexpected errors somewhere so you can keep track of them...
console.error(error);
return res.status(500).json({
error: "Sorry, something unexpected happened on our side."
});
}
});
The error object is the object that you threw earlier in your routes or middleware.
One thing to keep in mind: res.sendFile, res.render, res.compress, and res.json all return a Promise. Return that promise (or await it in an async handler) so the framework can route any rejection to your handleErr:
server.route("get", "/file", (req, res) => {
return res.sendFile("./images/sun.jpeg", "image/jpeg");
});
Or, in an async handler that does other work alongside the send:
server.route("get", "/avatars/:id", async (req, res) => {
const path = await db.lookupAvatarPath(req.params.id);
await res.sendFile(path);
});
Start listening on a specific port like this:
server.listen(3000, () => {
console.log("Server has started on port 3000");
});
server.listen accepts the same arguments as Node.js's http.Server.listen, so you can also pass a host, an options object, or a Unix socket path.
There are utility functions that you can include and use as middleware functions. These are meant to make it easier for you to build HTTP applications. In the future, many more will be added, and you only move them into memory once you include them. No need to have many npm dependencies for simple applications!
The list of utility functions as of now:
Including any one of them is done like this:
import cpeak, { utilName } from "cpeak";
With this middleware function, you can automatically set a folder in your project to b
$ claude mcp add cpeak \
-- python -m otcore.mcp_server <graph>