@harpiats/core
v1.0.0
Published
Harpia Framework is designed exclusively for the Bun runtime. Like Express, Fastify, or Hono, it can be used independently, offering a lightweight yet powerful foundation for building modern web applications optimized for Bun.
Maintainers
Readme
Harpia Framework
Harpia Core is the foundational module of the Harpia Framework, designed exclusively for the Bun runtime. Like Express, Fastify, or Hono, it can be used independently, offering a lightweight yet powerful foundation for building modern web applications optimized for Bun.
Table of Contents
Installation
bun add @harpiats/coreFeatures
- Route management
- Middleware support
- Web Socket support
- Static file support
- Template engine support
- Harpia template engine
- Custom "not found" route
- Custom cors
- Cookie handling
- Cache management
- Session management
- Upload
- Request monitor and performance metrics
- Test client (like Supertest)
- Memory storage and redis support
Examples
Start the server
import harpia from "@harpiats/core";
const app = harpia();
app.listen({
port: 3000,
development: true,
reusePort: true,
hostname: "localhost",
}, () => console.log("Server is running at http://localhost:3000/"));Server configuration
A Harpia application starts with the app.listen() method, which accepts a configuration object of type ServerOptions.
This object defines how the HTTP and WebSocket servers behave, including port, host, TLS settings, and connection performance options.
Example
import harpia from "@harpiats/core";
const app = harpia();
app.listen({
port: 3000,
development: true,
reusePort: true,
hostname: "localhost",
}, () => console.log("Server is running at http://localhost:3000/"));ServerOptions Reference
| Property | Type | Description |
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| port | number (optional but required if unix is not set) | The TCP port the server will listen on. |
| development | boolean (optional) | Enables development mode, which may include detailed error messages, automatic restarts, and verbose logging. |
| hostname | string (optional) | The hostname or IP address where the server should bind. Defaults to "0.0.0.0" (all interfaces). |
| tls | TLSOptions (optional) | Enables HTTPS by providing TLS/SSL configuration (e.g., cert, key). When present, the server will start over HTTPS instead of HTTP. |
| unix | string (optional but required if port is not set) | Path to a Unix domain socket file. If specified, the server will listen on this socket instead of a TCP port. |
| reusePort | boolean (optional) | When true, allows multiple processes to listen on the same port. Useful for load balancing across workers. |
| maxRequestBodySize | number (optional) | Maximum size (in bytes) of an incoming HTTP request body. Requests exceeding this limit will be rejected. |
| ws | object (optional) | Defines advanced options for WebSocket behavior and resource limits |
WebSocket Configuration (ServerOptions.ws)
The ws property defines advanced options for WebSocket behavior and resource limits.
| Property | Type | Description |
| ---------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| maxPayloadLength | number (optional) | Maximum size (in bytes) allowed for an incoming WebSocket message. Messages exceeding this limit will be dropped. |
| idleTimeout | number (optional) | Time in seconds a WebSocket connection can stay idle before being automatically closed. |
| backpressureLimit | number (optional) | The maximum amount of unsent data (in bytes) a WebSocket can buffer before triggering backpressure handling. |
| closeOnBackpressureLimit | boolean (optional) | When true, the server will automatically close connections that exceed the backpressure limit. |
| sendPings | boolean (optional) | When true, the server will automatically send periodic ping frames to connected clients to detect broken connections. |
| publishToSelf | boolean (optional) | Determines whether messages published to a channel are also delivered to the sender. Useful for broadcast implementations. |
| perMessageDeflate | object (optional) | Controls compression for WebSocket messages using the permessage-deflate extension. |
| perMessageDeflate.compress | boolean (optional) | Enables compression for outgoing WebSocket messages. |
| perMessageDeflate.decompress | boolean (optional) | Enables decompression for incoming WebSocket messages. |
Notes
- If both
portandunixare provided, the server will prefer the Unix socket. - WebSocket options (
ws) apply globally to all routes usingapp.ws()orrouter.ws(). - TLS options must include valid certificate and private key paths for secure HTTPS/WebSocket connections.
reusePortis typically used in production setups with multiple workers or clustered processes.
Route management
Creating a route in a different file.
import { Router } from "@harpiats/core";
const books = Router();
books.get("/books", () => console.log("Books route"));
export default books;Creating a route with a prefix
import { Router } from "@harpiats/core";
const books = Router("books");
books.get("/", () => console.log("Books route"));
export default books;Creating a websocket route
import { Router } from "@harpiats/core";
const routes = Router();
// Define a custom type for WebSocket connection data
type CustomWebSocketData = {
userId: string;
username: string;
sessionId: string;
};
// Create a WebSocket route for the chat
routes.ws<CustomWebSocketData>("/chat", {
// Called when a new WebSocket connection is opened
open(ws) {
const data = ws.data;
console.log("New WebSocket connection opened on /chat");
// Set custom data for this connection
data.userId = "123";
data.username = "Alice";
data.sessionId = "abc";
ws.send(`Welcome to the chat, ${data.username}!`);
},
// Called when a message is received from the client
message(ws, message) {
const data = ws.data;
console.log(`Message received from ${data.username}: ${message}`);
},
// Called when the WebSocket connection is closed
close(ws, code, reason) {
const data = ws.data;
console.log(`Connection closed (${code}) by ${data.username}: ${reason}`);
},
// Called when the socket is ready to send more data
drain(ws) {
const data = ws.data;
console.log(`Socket for ${data.username} is ready to send data`);
},
// Called when an error occurs on the WebSocket connection
error(ws, error) {
const data = ws.data;
console.error(`Error on ${data.username}'s connection:`, error);
},
});
export default routes;Import the route into the main application.
import harpia from "@harpiats/core";
import books from "./books.routes";
const app = harpia();
app.routes(books);
app.listen({
port: 3000,
development: true,
reusePort: true,
hostname: "localhost",
}, () => console.log("Server is running at http://localhost:3000/"));Middlewares
Set a global middleware
const app = harpia();
app.use((req, res, next) => {
// Your middleware logic
next();
});Set a middleware with path
app.use("/panel", (req, res, next) => {
// Your middleware logic
next();
});Set a middleware to a specific route
import { Router } from "@harpiats/core";
const books = Router();
books.get(
"/books",
() => console.log("auth middleware"),
() => console.log("Books route")
);
export default books;Web Socket
You can define a route and a custom data for each WebSocket connection and implement handlers for events like connection opening, message reception, connection closing, and errors.
You can define WebSocket routes that handle events for real-time connections.
Each connection creates its own ServerWebSocket instance, which can hold custom connection data using the ws.data property.
You can create WebSocket routes using either the application instance (app) or a router instance (Router):
import { Router } from "@harpiats/core";
const routes = Router();
routes.ws("/chat", { /** handlers */ });or
import harpia from "@harpiats/core";
const app = harpia();
app.ws("/chat", { /** handlers */ });Supported Handlers
| Handler | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| open(ws) | Called when a new WebSocket connection is successfully opened. |
| message(ws, message) | Called whenever a message is received from the client. The message can be a string, ArrayBuffer, or Uint8Array. |
| close(ws, code, reason) | Called when the connection is closed. Receives the close code and an optional reason message. |
| drain(ws) | Called when the socket is ready to receive more data (after backpressure is relieved). |
| error(ws, error) | Called when an error occurs on the WebSocket connection (e.g., failure to send data). |
Example with Custom Connection Data
You can define a custom data type for each WebSocket connection using ws.data.
This is useful for storing user info, session data, or connection-specific context.
// Define a custom type for WebSocket connection data
type CustomWebSocketData = {
userId: string;
username: string;
sessionId: string;
};
// Create a WebSocket route for the chat
app.ws<CustomWebSocketData>("/chat", {
// Called when a new WebSocket connection is opened
open(ws) {
const data = ws.data;
console.log("New WebSocket connection opened on /chat");
// Set custom data for this connection
data.userId = "123";
data.username = "Alice";
data.sessionId = "abc";
ws.send(`Welcome to the chat, ${data.username}!`);
},
// Called when a message is received from the client
message(ws, message) {
const data = ws.data;
console.log(`Message received from ${data.username}: ${message}`);
},
// Called when the WebSocket connection is closed
close(ws, code, reason) {
const data = ws.data;
console.log(`Connection closed (${code}) by ${data.username}: ${reason}`);
},
// Called when the socket is ready to send more data
drain(ws) {
const data = ws.data;
console.log(`Socket for ${data.username} is ready to send data`);
},
// Called when an error occurs on the WebSocket connection
error(ws, error) {
const data = ws.data;
console.error(`Error on ${data.username}'s connection:`, error);
},
});Key Notes
ws.datais persistent for the entire lifetime of a connection — each connected client has its own data object.- You can send messages with
ws.send("text")or send binary data (Uint8Array). - To broadcast messages to all connected clients, keep a global list of open connections and iterate through them.
- The
errorhandler is optional but highly recommended in production environments. - Harpia provides native WebSocket integration — no additional libraries like
wsorsocket.ioare required for basic functionality.
Static Files
To serve static files, you need to create a folder for them—e.g., public.
const app = harpia();
app.static("public");Template Engine
To set up a template engine, you can follow these steps:
Create a file for engine configuration:
// src/ejs.ts
import ejs from "ejs";
import path from "node:path";
import type { Harpia } from "@harpiats/core";
export const ejsEngine = {
configure: (app: Harpia) => {
app.engine.set(ejsEngine);
},
render: async (view: string, data: Record<string, any>) => {
const filePath = path.resolve(process.cwd(), "src/views", `${view}.ejs`);
return await ejs.renderFile(filePath, data);
}
};Set up the application to use the engine:
import harpia from "@harpiats/core";
import { ejsEngine } from "./ejs";
const app = harpia();
ejsEngine.configure(app);
app.get("/books", async (req, res) => {
await res.render("home", { title: "Books" })
});
app.listen...Sample EJS Template (e.g., src/views/home.ejs):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>To use the harpia template engine, you can follow these steps:
create a template-engine.ts file:
import path from "node:path";
import { TemplateEngine } from "@harpiats/core/template-engine";
const baseDir = process.cwd();
export const engine = new TemplateEngine({
viewName: "page", // page.html will be rendered
useModules: false, // true if uses a module structure (e.g. modules/users/pages/home/page.html)
fileExtension: ".html", // The default is `.html`, but you can use `.txt`, `.hml`, or any other.
path: {
views: path.join(baseDir, "src", "resources", "pages"),
layouts: path.join(baseDir, "src", "resources", "layouts"), // optional
components: path.join(baseDir, "src", "resources", "components"), // optional
},
});And set up the application to use the engine:
import harpia from "@harpiats/core";
import { engine } from "./template-engine";
const app = harpia();
engine.configure(app);
app.get("/books", async (req, res) => {
await res.render("home", { title: "Books" });
});
app.listen...If you want to use the Security Header Protection:
import harpia from "@harpiats/core";
import { shield } from "./shield";
import { engine } from "./template-engine";
const app = harpia();
// Apply security headers middleware
app.use(shield.middleware(app));
// Setup template engine
engine.configure(app, shield.instance);
// Routes
app.get("/books", async (req, res) => {
await res.render("home", { title: "Books" });
});
app.listen...If you would like use a module structure, e.g. modules/users/pages/home/page.html, then create a template-engine.ts file:
import path from "node:path";
import { TemplateEngine } from "@harpiats/core/template-engine";
const baseDir = process.cwd();
export const engine = new TemplateEngine({
viewName: "page", // page.html will be rendered
useModules: true, // true if uses a module structure (e.g. modules/users/pages/home/page.html)
fileExtension: ".html", // The default is `.html`, but you can use `.txt`, `.hml`, or any other.
path: {
views: path.join(baseDir, "modules", "**", "pages"),
layouts: path.join(baseDir, "resources", "layouts"),
components: path.join(baseDir, "resources", "components"),
},
});And set up the application to use the engine:
import harpia from "@harpiats/core";
import { engine } from "app/config/template-engine";
const app = harpia();
engine.configure(app);
app.get("/books", async (req, res) => {
await res.module("books").render("home", { title: "Books" });
});
app.listen...It is also possible to render a template from its path, regardless of where it is in the application. To do this, we can follow the example:
import harpia from "@harpiats/core";
import { engine } from "app/config/template-engine";
const app = harpia();
engine.configure(app);
app.post("/send-email", async (req, res) => {
const data = {}
const content = await engine.generate("app/services/mailer/templates/account-created", { data });
console.log(content);
});
app.listen...Harpia Template Engine Syntax Overview
| Type | Syntax | Description | Example |
| ------------------------------ | ------------------------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Variable / Output | {{ expr }} | Inserts the result of an expression into the template. | <h1>{{ title }}</h1> |
| Plugin / Function | {{ fn(arg1, arg2) }} | Calls a registered plugin or function and inserts its result. | <p>{{ uppercase(username) }}</p> |
| Local Assignment | @set var = value @endset | Declares a local variable available within the template scope. | @set name = "Lucas" @endset\n<p>{{ name }}</p> |
| Conditional | @if condition ... @elseif ... @else ... @endif | Defines conditional logic with optional branches. | @if isAdmin\n <p>Admin</p>\n@else\n <p>User</p>\n@endif |
| Array Loop | @for item in items ... @endfor | Iterates over an array; item represents the current element. | @for item in items\n <li>{{ item }}</li>\n@endfor |
| Object Loop | @for [key, value] in obj ... @endfor | Iterates over key/value pairs in an object. | @for [k, v] in obj\n <li>{{ k }}: {{ v }}</li>\n@endfor |
| Layout / Inheritance | @layout("default", { key: value }) | Defines the base layout and optionally passes static parameters. | @layout("default", { title: "Homepage" }) |
| Block Placeholder | @yield("block_name") | Defines a placeholder inside the layout for injected content. | <body>\n @yield("content")\n</body> |
| Content Block | @block("block_name") ... @endblock | Defines the content for a named block to be inserted into a layout. | @block("content")\n <h1>Hello</h1>\n@endblock |
| Import/Include | @import("component", { key: value }) | Includes another template relative to the current view, with props. | @import("button", { text: "Save" }) |
| Partial/Component | @component("name", { key: value }) | Includes a reusable component from the components directory. | @component("header", { user: currentUser }) |
| Comment | ## comment | Defines a comment line that is not rendered in the final HTML. | ## This is a comment |
Plugin Usage Example:
import path from "node:path";
import { TemplateEngine } from "@harpiats/core/template-engine";
const baseDir = process.cwd();
export const engine = new TemplateEngine({
viewName: "page",
useModules: false,
fileExtension: ".html",
path: {
views: path.join(baseDir, "src", "resources", "pages"),
layouts: path.join(baseDir, "src", "resources", "layouts"),
components: path.join(baseDir, "src", "resources", "components"),
},
});
// Register custom plugins
engine.registerPlugin("uppercase", (str: string) => str.toUpperCase());
engine.registerPlugin("sum", (a: number, b: number) => a + b);and in the .html file:
<p>Uppercase plugin: {{ uppercase(user.name) }}</p>
<p>Sum plugin: {{ sum(10, 20) }}</p>Layout Example
Layout name: default.html
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
@yield("content")
</body>
</html>View:
@layout("default", { title: "Homepage" })
@block("content")
<h1>Welcome, {{ user.name }}!</h1>
<p>{{ uppercase("this page uses the default layout.") }}</p>
@endblockUnescaped Content
Using the raw(...) plugin, you can add content without escaping.
<p>{{ raw("<script>alert('xss')</script>") }}</p>Nonce Value
When using the Shield module for security headers, the template engine automatically registers a generateNonce plugin. This plugin generates a unique nonce for each request, which you can use to secure inline scripts and styles for Content Security Policy (CSP).
<script nonce="{{ nonce }}">
console.log("This script is CSP-compliant");
</script>The
generateNonceplugin is automatically available whenapp.shield()is configured in the application. See the Shield section for setup instructions.
Method Override
The Method Override technique is commonly used to simulate HTTP methods like PUT, DELETE, and PATCH in web applications where the client (e.g., browsers) may not natively support these methods. This is particularly useful when working with HTML forms, which only support GET and POST methods.
The idea is to include a hidden input field (e.g., _method) within a POST form to indicate the desired HTTP method. The form must use the enctype="application/x-www-form-urlencoded" attribute to ensure the data is properly encoded. When the form is submitted, the server reads the _method value and overrides the actual POST method with the specified one.
Example
<form action="/account" method="POST" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">Delete</button>
</form>Custom not found route
You can define a custom "Not Found" route to handle requests to undefined paths.
const app = harpia();
app.setNotFound((req, res) => {
res.json({ status: 404, message: "Not Found" });
});If you want to specify a particular HTTP method for the "Not Found" route (e.g., only for GET requests), you can do it as follows:
const app = harpia();
app.setNotFound((req, res) => {
res.json({ status: 404, message: "Not Found" });
}, ["GET"]);Note: Currently, the framework does not support multiple "Not Found" routes for different HTTP methods or paths. You can only define one "Not Found" route.
Cors
Harpia provides a way to configure Cross-Origin Resource Sharing (CORS) for your application, allowing you to specify which origins, HTTP methods, and headers are permitted.
You can define CORS settings globally for the entire application or specify them for individual routes.
Basic CORS Setup
To set up basic CORS for your application:
import harpia from "@harpiats/core";
const app = harpia();
app.cors({
origin: "*", // Allow all origins
methods: ["GET", "POST", "PUT", "DELETE"], // Allow specific HTTP methods
allowedHeaders: ["Content-Type", "Authorization"], // Allow specific headers
});
app.listen...Customizing CORS for Specific Routes
You can also apply different CORS configurations to specific routes or groups of routes.
import harpia, { Router } from "@harpiats/core";
const books = Router();
books.get("/books", () => {
console.log("Books route with custom CORS");
});
const app = harpia();
app.routes(books);
app.cors({
origin: "https://example.com", // Allow only this origin
methods: "GET", // Allow only GET method
}, "/books"); // Apply CORS only to the "/books" route
app.listen...CORS Options
You can configure the following options for CORS:
origin: Specifies the allowed origins. Can be a boolean (trueto allow all), a string (a specific origin), a regular expression, or an array of origins or regular expressions.Example:
true: Allow all origins."https://example.com": Allow onlyexample.com./^https:\/\/.*\.com$/: Allow any.comdomain.
methods: Defines the allowed HTTP methods. It can be a single method ("GET","POST", etc.), an array of methods, or"*", which means all methods are allowed.allowedHeaders: Specifies which headers can be included in the request. It can be a string (e.g.,"Content-Type") or an array of headers.exposedHeaders: Allows you to specify which headers can be exposed to the browser. It can be a string or an array of headers.credentials: Indicates whether the browser should send cookies and HTTP authentication information with cross-origin requests. Set totrueto allow credentials.maxAge: Specifies the maximum time (in seconds) that the browser should cache the CORS response.preflightContinue: Iftrue, the framework will not respond to preflight requests automatically; this must be handled by the user manually.optionsSuccessStatus: Allows you to specify a custom success status code for preflight requests (defaults to204).
Example with All Options
import type { CorsOptions } from 'harpia';
const corsOptions: CorsOptions = {
origin: "https://example.com", // Allow only requests from example.com
methods: ["GET", "POST"], // Allow only GET and POST methods
allowedHeaders: ["Content-Type", "Authorization"], // Allow specific headers
exposedHeaders: ["X-Custom-Header"], // Expose custom headers to the client
credentials: true, // Allow credentials (cookies) in cross-origin requests
maxAge: 3600, // Cache the CORS preflight response for 1 hour
optionsSuccessStatus: 200, // Custom success status code for preflight requests
}
app.cors(corsOptions);Cookies
You can use this to store session information, authentication tokens, or any other data that needs to persist across requests.
Setting a Cookie
To set a cookie, you can use the res.cookies.set method. You can specify various options such as expiration date, domain, path, and security settings.
import harpia, { type CookiesOptions } from "@harpiats/core";
const app = harpia();
app.get("/set-cookie", (req, res) => {
const cookieOptions: CookiesOptions = {
path: "/",
maxAge: 3600, // 1 hour
secure: true, // Only send cookie over HTTPS
httpOnly: true, // Make the cookie inaccessible to JavaScript
sameSite: "Strict", // Restrict the cookie to first-party contexts
};
res.cookies.set("userSession", "abcd1234", cookieOptions); // Set a cookie with options
res.send("Cookie has been set!");
});
app.listen...Getting a Cookie
To retrieve a cookie, you can use the get method. It returns the value of the specified cookie if it exists, or undefined if the cookie is not found.
app.get("/get-cookie", (req, res) => {
const userSession = req.cookies.get("userSession"); // Get the cookie
if (userSession) {
res.send(`User session: ${userSession}`);
} else {
res.send("No user session cookie found.");
}
});Deleting a Cookie
To delete a cookie, you can use the delete method. This sets the cookie value to an empty string and expires it immediately.
app.get("/delete-cookie", (req, res) => {
res.cookies.delete("userSession"); // Delete the cookie
res.send("Cookie has been deleted.");
});Cookie Options
You can customize cookies using the following options:
path: The URL path for which the cookie is valid. Defaults to the root path (/).domain: The domain for which the cookie is valid. Defaults to the current domain.maxAge: The maximum age of the cookie in seconds. If not set, the cookie will be a session cookie, expiring when the browser closes.expires: The exact expiration date for the cookie. Can be set as aDateobject.httpOnly: Iftrue, the cookie will be inaccessible to JavaScript, protecting it from cross-site scripting (XSS) attacks.secure: Iftrue, the cookie will only be sent over HTTPS connections, ensuring its security.sameSite: Controls whether the cookie should be sent with cross-origin requests. Possible values are:"Strict": The cookie will only be sent in a first-party context (i.e., when navigating to the origin site)."Lax": The cookie will be sent for top-level navigations and GET requests."None": The cookie will be sent with all requests, including cross-origin requests. If using"None", thesecureflag must also be set totrue.
Example of using SameSite:
const cookieOptions: CookiesOptions = {
sameSite: "Strict", // Only send the cookie in a first-party context
secure: true, // Send only over HTTPS
};
res.cookie("userSession", "abcd1234", cookieOptions);Getting All Cookies
To retrieve all cookies in the request, you can use the getAll method, which returns a dictionary of cookie names and values.
app.get("/get-all-cookies", (req, res) => {
const allCookies = req.cookies.getAll(); // Get all cookies
res.json(allCookies);
});Cache
Harpia includes a Cache class that allows you to store and manage cached data for your web applications.
Creating a Cache Instance
To create a new cache instance, simply instantiate the Cache class. You can optionally provide a custom store (e.g., using a custom memory store or a third-party store like Redis).
import harpia, { Cache } from "@harpiats/core";
const app = harpia();
const cache = new Cache();Storing and Retrieving Data in Cache via Routes
You can store data in the cache or retrieve it from the cache within route handlers. Here's an example of caching a value in a route and retrieving it from the cache in a different route:
import harpia, { Cache } from "@harpiats/core";
const app = harpia();
const cache = new Cache();
// Route to set data in the cache
app.get("/set-cache", async (req, res) => {
const userProfile = { username: "john_doe", age: 30 };
await cache.set("userProfile", userProfile);
res.json({ message: "User profile cached!" });
});
// Route to retrieve data from the cache
app.get("/get-cache", async (req, res) => {
const userProfile = await cache.get("userProfile");
if (!userProfile) {
res.json({ status: "not found", message: "User profile not found in cache." });
return;
}
res.json({ status: "success", data: userProfile });
});
app.listen(3000, () => console.log("Server started at http://localhost:3000/"));In the example above:
/set-cachestores a user profile object in the cache under the keyuserProfile./get-cacheretrieves the cached user profile if available. If it's not found, it returns a "not found" message.
Deleting Data from Cache via Route
You can also delete cached data using a route. Here's an example of how to delete a cache entry:
app.get("/delete-cache", async (req, res) => {
await cache.delete("userProfile");
res.json({ message: "User profile cache deleted!" });
});Example Usage
Here's an example of using caching in a real-world scenario, such as caching user profile data for performance optimization. In this case, if the user profile is already cached, it will be returned from the cache instead of fetching it again from the database.
app.get("/user-profile/:id", async (req, res) => {
const userId = req.params.id;
const cacheKey = `userProfile:${userId}`;
// Try to fetch the user profile from the cache
const cachedProfile = await cache.get(cacheKey);
if (cachedProfile) {
res.json({ status: "success", data: cachedProfile });
return;
}
// If not in cache, fetch the profile from the database (simulated here)
const profile = { id: userId, username: "john_doe", age: 30 };
// Store the profile in cache for future requests
await cache.set(cacheKey, profile);
// Respond with the fetched profile
res.json({ status: "success", data: profile });
});Session
Harpia includes a Session class that allows you to manage user sessions. The Session class utilizes a store (default is in-memory) to manage session data, and a cookie is used to track the session ID on the client-side.
Creating a Session Instance
To use the session management functionality, you first need to create an instance of the Session class.
import harpia, { Session } from "@harpiats/core";
const app = harpia();
const session = new Session();Creating a New Session
You can create a new session with a given data object. When a session is created, a session ID is generated and the session data is stored.
app.get("/login", async (req, res) => {
const userData = { username: "john_doe", role: "admin" };
const sessionId = await session.create(userData);
// Set the session ID in the cookie
session.setCookie(res, sessionId, { httpOnly: true, secure: true });
res.json({ message: "User logged in successfully!" });
});In this example, when a user logs in, a new session is created with the user data, and the session ID is stored in a cookie on the client.
Retrieving Session Data
You can retrieve session data from the store using the session ID stored in the client's cookies. The session ID is sent with each request in the cookie.
app.get("/profile", async (req, res) => {
const userSession = await session.fromRequest(req);
if (userSession) {
res.json({ status: "success", data: userSession });
} else {
res.json({ status: "not authenticated", message: "Please log in first." });
}
});Here, the session data is retrieved based on the session ID from the client's cookies. If the session exists, the user's data is returned; otherwise, the user is prompted to log in.
Updating Session Data
Session data can be updated by fetching the existing data and modifying it.
app.get("/update-profile", async (req, res) => {
const sessionId = req.cookies.get("session_id");
if (sessionId) {
const updatedData = { role: "super_admin" };
const success = await session.update(sessionId, updatedData);
if (success) {
res.json({ status: "success", message: "Profile updated." });
} else {
res.json({ status: "error", message: "Session not found." });
}
} else {
res.json({ status: "error", message: "Session not found." });
}
});In this example, the session data is updated by passing the new data. If the session is valid, the updated information is stored in the session.
Deleting a Session
When a user logs out or you want to clear the session data, you can delete the session both from the store and the client's cookies.
app.get("/logout", async (req, res) => {
const sessionId = req.cookies.get("session_id");
if (sessionId) {
await session.delete(sessionId, res);
res.json({ message: "User logged out successfully!" });
} else {
res.json({ message: "No active session found." });
}
});Here, the session is deleted, and the corresponding cookie is also cleared.
Custom Session Cookie Options
When setting the session cookie, you can specify options such as httpOnly, secure, and maxAge.
app.get("/set-cookie", (req, res) => {
const sessionId = "example-session-id";
session.setCookie(res, sessionId, { httpOnly: true, secure: true, maxAge: 3600 });
res.json({ message: "Session cookie set." });
});In this example, the session cookie is set with options that make it HTTP-only (not accessible via JavaScript) and secure (only sent over HTTPS). The maxAge option is also set to define how long the cookie should last (in seconds).
CSRF
The CSRF class provides a simple and lightweight mechanism for generating and validating CSRF (Cross-Site Request Forgery) tokens tied to user sessions. It includes built-in token expiration and supports either a custom store or a default in-memory store.
Setup
import { CSRF } from "@harpiats/core/csrf";
import { MemoryStore } from "@harpiats/core/memory-store";
const csrf = new CSRF({
store: new MemoryStore(),
ttl: 10 * 60 * 1000, // 10 minutes
});Parameters:
store(optional) — Custom store implementing theStoreinterface. Defaults toMemoryStore.ttl(optional) — Token lifetime in milliseconds. Defaults to 5 minutes (5 * 60 * 1000).
Methods
generate(sessionId: string): Promise<string>
Generates a new CSRF token for the specified session ID. If a token already exists for the session, it will be replaced.
Example:
const token = await csrf.generate("user-session-123");
console.log(token); // e.g. "a1b2c3d4..."check(sessionId: string, token: string): Promise<boolean>
Validates a CSRF token for a given session ID.
Returns true if the token matches and is not expired; otherwise returns false.
Parameters:
sessionId: The session identifier.token: The token to validate.
Example:
const isValid = await csrf.check("user-session-123", token);
console.log(isValid); // true or falsedelete(sessionId: string): Promise<void>
Deletes the CSRF token associated with the specified session ID.
Example:
await csrf.delete("user-session-123");Basic Usage
const csrf = new CSRF();
// Step 1: Generate a token for the user's session
const sessionId = "session-001";
const token = await csrf.generate(sessionId);
// Step 2: Send the token to the client (e.g., as a hidden form field)
// Step 3: When the client submits a form, verify the token
const isValid = await csrf.check(sessionId, token);
if (isValid) {
console.log("CSRF token is valid. Proceed with request.");
} else {
console.log("Invalid or expired CSRF token.");
}
// Step 4: Optionally delete the token after use
await csrf.delete(sessionId);Notes
- Tokens expire automatically based on the configured TTL.
- Always use HTTPS and proper session handling in production environments.
- The default
MemoryStoreis intended for testing or small applications; for distributed or persistent setups, use a custom store.
Upload
You can set up a middleware to manage single or multiple file uploads, specifying options like allowed file types, extensions, and maximum file size.
Setting Up the Upload Middleware
First, create an instance of the Upload module with your desired configuration:
import { Upload } from "@harpiats/core/upload";
export const upload = new Upload({
fieldName: "file", // Field name for the file in the request
prefix: "profile", // Prefix for the uploaded file name
fileName: Date.now().toString(), // Custom file name (e.g., using a timestamp)
path: "tmp", // Directory to save the uploaded files
options: {
allowedExtensions: [".jpg"], // Allowed file extensions
allowedTypes: ["image/jpeg"], // Allowed MIME types
maxSize: 1024 * 1024 * 2, // Maximum file size (2MB in this case)
},
});Using the Upload Middleware in Routes
Once the Upload instance is configured, you can use it as middleware in your routes to handle file uploads.
For Single File Uploads:
app.post("/user", upload.single, async (req, res) => {
// Handle the uploaded file here
});For Multiple File Uploads:
app.post("/user", upload.multiple, async (req, res) => {
// Handle the uploaded files here
});Telemetry
Harpia includes a built-in Telemetry module that acts as an observability-as-a-service tool. It tracks server traffic, page views, response times, errors, and visitor behavior out of the box, offering a comprehensive Reading API to build internal dashboards without needing third-party analytics tools.
Each visit is stored as a unified VisitData object, keeping path, timestamp, response time, error details, and traffic source together in a single, self-contained record.
Setup
Instantiate the Telemetry class and add handleRequest as a global middleware:
import harpia, { Telemetry } from "@harpiats/core";
const app = harpia();
const telemetry = new Telemetry({
ignore: ["/favicon.ico", "/healthcheck"], // Paths to exclude from monitoring
trustProxy: true, // Enable if behind Nginx/Cloudflare to resolve real IPs
});
app.use(async (req, res, next) => {
await telemetry.initialize(req, app.requestIP() || "unknown");
const telemetryRes = await telemetry.handleRequest();
if (telemetryRes) return telemetryRes;
next();
});Tracking Traffic Sources
Pass a TrafficSource object as the third argument to initialize() to record UTM attribution data per visit:
app.use(async (req, res, next) => {
const url = new URL(req.url);
const trafficSource = {
utm: {
source: url.searchParams.get("utm_source") ?? undefined,
medium: url.searchParams.get("utm_medium") ?? undefined,
},
};
await telemetry.initialize(req, app.requestIP() || "unknown", trafficSource);
const telemetryRes = await telemetry.handleRequest();
if (telemetryRes) return telemetryRes;
next();
});Security
Protect the Reading API with a token and/or an allowlist of IPs:
const telemetry = new Telemetry({
accessToken: process.env.TELEMETRY_TOKEN,
allowedIps: ["127.0.0.1", "10.0.0.5"],
});Pass token and callerIp inside the parameter object of any read method. An invalid token or disallowed IP will throw an Unauthorized error.
Options Reference
| Property | Type | Description |
| --- | --- | --- |
| store | Store (optional) | Custom storage backend. Defaults to MemoryStore. Use RedisStore for persistence across restarts. |
| ignore | string[] (optional) | Paths to skip tracking. |
| trustProxy | boolean (optional) | When true, resolves the real client IP from x-forwarded-for, cf-connecting-ip, or x-real-ip headers. |
| maxVisitorsKeys | number (optional) | Maximum number of unique IPs stored per day. Defaults to 5000. |
| accessToken | string (optional) | Secret token required on all read methods. |
| allowedIps | string[] (optional) | Allowlist of IPs permitted to call read methods. |
Reading API
All methods accept a single configuration object. Security properties (token, callerIp) are always optional inside this object.
getAll({ token?, callerIp? })
Returns the complete raw telemetry data stored in the backend.
const data = await telemetry.getAll({ token, callerIp });{
"access": {
"totalRequests": 1500,
"visitorsByDate": {
"2023-10-25": {
"192.168.1.1": {
"totalRequests": 2,
"visits": [
{ "path": "/home", "timestamp": "2023-10-25T14:00:00.000Z", "responseTime": 45, "error": null },
{ "path": "/about", "timestamp": "2023-10-25T14:01:00.000Z", "responseTime": 30, "error": null }
]
}
}
}
},
"behavior": { "pageViews": { "/home": 850, "/about": 320 } }
}summary({ date?, limit?, token?, callerIp? })
Returns a high-level daily overview. limit controls the number of top pages returned (default: 10).
const stats = await telemetry.summary({ limit: 5, token, callerIp });{
"date": "2023-10-25",
"totalRequests": 1500,
"uniqueVisitors": 320,
"topPages": [
{ "path": "/home", "views": 850 },
{ "path": "/about", "views": 320 }
],
"avgResponseTime": 42.5,
"totalErrors": 2
}getDailyStats({ token?, callerIp? })
Returns an array with aggregated request and visitor counts per day. Ideal for trend charts.
const stats = await telemetry.getDailyStats({ token, callerIp });[
{ "date": "2023-10-23", "totalRequests": 980, "uniqueVisitors": 210 },
{ "date": "2023-10-24", "totalRequests": 1200, "uniqueVisitors": 280 }
]getVisitors({ date?, token?, callerIp? })
Returns all unique visitor records for a given date, keyed by IP.
const visitors = await telemetry.getVisitors({ date: "2023-10-25", token, callerIp });getVisitorByIp({ ip, date?, token?, callerIp? })
Returns all visit data for a specific IP address on a given date.
const visitor = await telemetry.getVisitorByIp({ ip: "192.168.1.1", token, callerIp });{
"totalRequests": 1,
"visits": [
{ "path": "/home", "timestamp": "2023-10-25T14:00:00.000Z", "responseTime": 45, "error": null }
]
}countUniqueVisitors({ date?, token?, callerIp? })
Returns the total number of unique IPs for a given date.
const count = await telemetry.countUniqueVisitors({ token, callerIp }); // => 320getPageViews({ date?, token?, callerIp? })
Returns a record mapping paths to their total view count.
const views = await telemetry.getPageViews({ date: "2023-10-25", token, callerIp });
// => { "/home": 850, "/about": 320 }getTopPages({ limit, date?, token?, callerIp? })
Returns the top limit most visited pages, sorted by views descending.
const top5 = await telemetry.getTopPages({ limit: 5, token, callerIp });
// => [{ path: "/home", views: 850 }, ...]getPageByPath({ path, date?, token?, callerIp? })
Returns detailed stats for a specific path, including average response time and error count.
const page = await telemetry.getPageByPath({ path: "/pricing", token, callerIp });{
"path": "/pricing",
"views": 310,
"visitors": ["192.168.1.1", "10.0.0.5"],
"avgResponseTime": 65.3,
"errorCount": 2
}getAvgResponseTime({ date?, token?, callerIp? })
Returns the average response time (ms) across all requests for a given date.
const avg = await telemetry.getAvgResponseTime({ token, callerIp }); // => 42.5getSlowRequests({ threshold, date?, token?, callerIp? })
Returns all requests that exceeded threshold milliseconds, sorted slowest-first. Includes path and timestamp for each entry.
const slow = await telemetry.getSlowRequests({ threshold: 500, token, callerIp });[
{ "ip": "10.0.0.5", "path": "/checkout", "timestamp": "2023-10-25T14:03:00.000Z", "responseTime": 1250 }
]getErrors({ date?, token?, callerIp? })
Returns a detailed list of all requests that resulted in an error, sorted newest-first. Each entry contains the structured error object with code and message.
const errors = await telemetry.getErrors({ token, callerIp });[
{
"ip": "10.0.0.5",
"path": "/checkout",
"timestamp": "2023-10-25T14:03:00.000Z",
"responseTime": 1250,
"error": { "code": 500, "message": "Payment gateway timeout" }
}
]countErrors({ date?, token?, callerIp? })
Returns the total number of errored requests for a given date.
const total = await telemetry.countErrors({ token, callerIp }); // => 15getTrafficSources({ source?, date?, token?, callerIp? })
Groups visit counts by source/medium based on recorded UTM parameters. Filter by a specific source to narrow results.
const sources = await telemetry.getTrafficSources({ source: "google", token, callerIp });
// => { "google/cpc": 450, "google/organic": 200 }flush({ token?, callerIp? })
Clears all telemetry data from the store. Use with caution.
await telemetry.flush({ token, callerIp });delete({ ip?, date?, token?, callerIp? })
Deletes records based on IP, date, or both. Useful for GDPR erasure requests or cleaning up test data.
// Delete all data for a specific date
await telemetry.delete({ date: "2023-10-25", token, callerIp });
// Delete all data for a specific IP across all dates
await telemetry.delete({ ip: "192.168.1.1", token, callerIp });
// Delete data for a specific IP on a specific date
await telemetry.delete({ ip: "192.168.1.1", date: "2023-10-25", token, callerIp });Security Headers (Shield)
The Shield module is designed to enhance the security of your application by automatically adding HTTP security headers to responses. These headers help protect against common web vulnerabilities such as cross-site scripting (XSS), clickjacking, and content injection.
Key Features
- Security Headers:
- Content Security Policy (CSP): Restricts sources for scripts, styles, and other resources.
- Cross-Origin Policies: Controls how resources are shared across origins.
- Strict Transport Security (HSTS): Enforces HTTPS connections.
- Referrer Policy: Controls the information sent in the Referer header.
- X-Content-Type-Options: Prevents MIME type sniffing.
- X-Frame-Options: Protects against clickjacking.
- X-XSS-Protection: Disables browser XSS filters (if not needed).
- Nonce Support: Generates cryptographic nonces for CSP inline scripts/styles.
- Customizable:
- Override default headers by passing options to the constructor.
- Merge custom directives with sensible defaults.
- Middleware:
- Easily integrate into your application as middleware.
- Template Engine Integration:
- Automatic nonce generation for secure inline scripts/styles.
- Seamless integration with the template engine.
Basic Usage
Configure your security headers and apply them to the application using the native app.shield() method.
import harpia from "@harpiats/core";
import type { SecurityHeaders } from "@harpiats/core";
const app = harpia();
const shieldOptions: SecurityHeaders = {
useNonce: true, // Enable nonce generation, false as default.
};
// Apply security headers
app.shield(shieldOptions);
// Your routes
app.get("/", (req, res) => {
res.send("Hello, World!");
});
app.listen({ port: 3000 }, () => console.log("Server running on http://localhost:3000"));If you want to use the Harpia template engine, the generateNonce plugin is automatically registered when app.shield() is initialized:
import harpia from "@harpiats/core";
import { engine } from "./template-engine";
const app = harpia();
// Apply security headers
app.shield({ useNonce: true });
// Setup template engine
engine.configure(app);To understand more about the Harpia template engine, see the Template Engine section.
Customizing Security Headers
You can customize the security headers by passing options to app.shield:
app.shield({
useNonce: true,
contentSecurityPolicy: {
directives: {
"default-src": ["'self'", "https://trusted.com"],
"script-src": ["'self'", "'unsafe-inline'"],
},
},
strictTransportSecurity: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
});Use nonce in templates:
<!-- Use @set to define a variable to hold the nonce value -->
@set nonce = generateNonce() @endset
<!DOCTYPE html>
<html>
<head>
<title>Secure Page</title>
</head>
<body>
<h1>Hello, {{ name }}!</h1>
<!-- Secure inline script with nonce -->
<script nonce="{{ nonce }}">
console.log("This script is CSP-compliant");
</script>
<!-- Secure inline styles with nonce -->
<style nonce="{{ nonce }}">
body { color: blue; }
</style>
</body>
</html>
generateNonce()is a Template Engine Plugin. You can see more about the plugins in Template Engine section.
Important Notes
- Nonce Security: Each nonce is unique per request and automatically invalidated after use. Since the
generateNonce()plugin generates a new value on each call, you should cache it using@setif you need to use it in multiple places. - CSP Compliance: Use @set nonce = generateNonce() @endset to cache the nonce value in templates.
- Development: Consider adding 'unsafe-inline' for easier development with hot reload.
- Production: Remove unsafe directives and rely exclusively on nonces for inline content.
The Shield module provides enterprise-grade security headers with zero configuration while remaining fully customizable for your specific needs.
Test Client
The Test Client is a powerful tool for testing your application's routes. It supports all HTTP methods (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD), query parameters, headers, JSON payloads, form data, and file uploads. Below is a detailed explanation of its features and usage.
Key Features
HTTP Methods:
.get(url)- Simulate aGETrequest..post(url)- Simulate aPOSTrequest..put(url)- Simulate aPUTrequest..delete(url)- Simulate aDELETErequest..patch(url)- Simulate aPATCHrequest..options(url)- Simulate anOPTIONSrequest..head(url)- Simulate aHEADrequest.
Query Parameters:
.query(name, value)- Add query parameters to the request.
Headers:
.set(name, value)- Set custom headers for the request.
Request Body:
.send(data)- Send raw data in the request body..json(data)- Send JSON data in the request body..formData(data)- Send form data in the request body.
File Uploads:
.file(files)- Upload a single file or multiple files..files(files)- Upload multiple files for a single field.
Execution:
.execute()- Execute the request and return the response.
Example Usage
Testing a GET Request with Query Parameters
import { expect, test } from "bun:test";
import { TestClient } from "@harpiats/core";
import { app } from "start/server";
test("GET /hello returns status 401", async () => {
const request = new TestClient(app)
.get("/hello")
.query("number", "123456"); // Add query parameter
const response = await request.execute();
// Validate the response
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ message: "Unauthorized" });
});Testing a POST Request with JSON Payload
test("POST /user returns status 201", async () => {
const request = new TestClient(app)
.post("/user")
.json({ name: "John", age: 30 }); // Send JSON data
const response = await request.execute();
// Validate the response
expect(response.status).toBe(201);
expect(await response.json()).toEqual({ id: 1, name: "John", age: 30 });
});Testing a POST Request with File Upload
import path from "node:path";
test("POST /upload handles file upload", async () => {
const filePath = path.resolve(__dirname, "./uploads/image.jpg");
const request = new TestClient(app)
.post("/upload")
.file({ file: filePath }) // Upload a single file
.formData({ username: "example" }); // Add form data
const response = await request.execute();
console.log(await response.json()); // Log the response
});Testing a POST Request with Multiple Files
test("POST /upload handles multiple files", async () => {
const filePath1 = path.resolve(__dirname, "./uploads/image1.jpg");
const filePath2 = path.resolve(__dirname, "./uploads/image2.jpg");
const request = new TestClient(app)
.post("/upload")
.files({ files: [filePath1, filePath2] }); // Upload multiple files
const response = await request.execute();
console.log(await response.json()); // Log the response
});Advanced Usage
Setting Custom Headers
test("GET /protected requires authorization", async () => {
const request = new TestClient(app)
.get("/protected")
.set("Authorization", "Bearer token123"); // Set custom header
const response = await request.execute();
expect(response.status).toBe(200);
});Sending Raw Data
test("POST /raw sends raw data", async () => {
const request = new TestClient(app)
.post("/raw")
.send("raw data"); // Send raw data
const response = await request.execute();
expect(response.status).toBe(200);
});Response Handling
The .execute() method returns includes:
- Status Code:
response.status - Headers:
response.headers - Body:
response.json(),response.text(), orresponse.blob()
Error Handling
- If you try to mix incompatible body types (e.g.,
.json()after.formData()), an error will be thrown. - If a file path does not exist, an error will be thrown.
Memory Storage
The Memory Storage module provides an in-memory key-value store for managing session data or other temporary storage needs. It implements the Store interface, offering methods to get, set, and delete data.
Storing and Retrieving Data
const memoryStore = new MemoryStore();
// Store session data
await memoryStore.set("session123", { userId: 1, username: "Alice" });
// Retrieve session data
const sessionData = await memoryStore.get("session123");
console.log(sessionData); // { userId: 1, username: "Alice" }
// Delete session data
await memoryStore.delete("session123");
console.log(await memoryStore.get("session123")); // undefinedUsing with Session Management
import { MemoryStore } from "@harpiats/core/memory-store";
import { SessionManager } from "@harpiats/core/session";
const memoryStore = new MemoryStore();
const sessionManager = new SessionManager({ store: memoryStore });
// Create a new session
const sessionId = await sessionManager.createSession({ userId: 1, username: "Alice" });
// Retrieve session data
const session = await sessionManager.getSession(sessionId);
console.log(session); // { userId: 1, username: "Alice" }
// Delete the session
await sessionManager.deleteSession(sessionId);Redis
The Redis Storage provides a persistent key-value store using Redis. It implements the Store interface, offering methods to get, set, and delete data. Redis is ideal for distributed systems, caching, and persistent session storage.
Implementation
import type { Store } from "@harpiats/core";
import { RedisClient } from "bun";
export class RedisStore implements Store {
private client: RedisClient;
private ready: Promise<void>;
constructor(db?: number) {
const host = process.env.REDIS_HOST || "localhost";
const port = process.env.REDIS_PORT || "6379";
const user = process.env.REDIS_USER || "";
const pass = process.env.REDIS_PASS || "";
const auth = user || pass ? `${encodeURIComponent(user)}:${encodeURIComponent(pass)}@` : "";
let url = `redis://${auth}${host}:${port}`;
if (db !== undefined && db !== 0) {
url = `${url.replace(/\/\d+$/, "")}/${db}`;
}
this.client = new RedisClient(url, {
autoReconnect: true,
maxRetries: 10,
connectionTimeout: 10000,
});
this.client.onconnect = () => console.log("Connected to Redis");
this.client.onclose = (err) => console.error("Redis error:", err);
this.ready = this.client.connect();
}
async on(): Promise<boolean> {
try {
await this.ready;
return this.client.connected;
} catch {
return false;
}
}
async get(key: string): Promise<Record<string, any> | undefined> {
const data = await this.client.get(key);
return data ? JSON.parse(data) : undefined;
}
async set(key: string, data: any): Promise<void> {
await this.client.set(key, JSON.stringify(data));
}
async setEx(key: string, data: any, ttlSeconds: number): Promise<void> {
await this.client.send("SET", [key, JSON.stringify(data), "EX", String(ttlSeconds)]);
}
async delete(key: string): Promise<void> {
await this.client.del(key);
}
}Example Usage
Once you've set up the RedisStore, you can use it in your routes to manage sessions. Below is an example of how to use Redis to manage user sessions in a Harpia app:
import { Router, Session } from "@harpiats/core";
import { RedisStore } from "redis.ts";
// Redis Setup
const redisStore = new RedisStore();
const useSession = new Session({ store: redisStore, cookieName: "my_session_id" });
// Routes Setup
const session = Router();
session.post("/login", async (req, res) => {
const sessionExists = await useSession.fromRequest(req);
if (sessionExists) {
res.json({ message: "Session does not exists." });
} else {
const sessionData = { userId: "12345", username: "pyro" };
const sessionId = await useSession.create(sessionData);
useSession.setCookie(res, sessionId, { maxAge: 3600, httpOnly: true, secure: true });
res.cookies.set("theme", "dark");
res.json({ message: "Successful login!" });
}
});
session.get("/profile", async (req, res) => {
const sessionData = await useSession.fromRequest(req);
if (sessionData) {
res.json({ profile: sessionData });
} else {
res.status(401).json({ message: "Session expired or not found!" });
}
});
session.post("/logout", async (req, res) => {
const sessionId = req.cookies.get("my_session_id");
if (sessionId) {
await useSession.delete(sessionId, res);
res.json({ message: "Successful logout!" });
} else {
res.status(400).json({ message: "Session not found!" });
}
});
export { session };How It Works:
- Login Route (
/login): When a user logs in, a new session is created with the user's data. The session ID is stored in a Redis database and sent to the client as a cookie. If a session already exists, the user is notified. - Profile Route (
/profile): The user's session data is fetched from Redis using the session ID stored in the client's cookie. If the session is valid, the user's profile data is returned. - Logout Route (
/logout): When the user logs out, the session is deleted from Redis, and the session cookie is cleared from the client.
This integration provides a persistent session management system backed by Redis, allowing you to scale your application efficiently. The session data is stored securely and can be accessed across different instances of your application.
