| |

TypeScript 91 🔷 TypeScript with Node and Express

TypeScript and Node.js were not designed together. Node.js was built to run JavaScript, and for most of its history, adding TypeScript meant compiling first or using a third-party runtime like ts-node or tsx. That changed with Node.js 22.6, when type stripping was introduced as an experimental feature, and it became stable in Node.js 24.12 . Node.js can now execute .ts files directly, stripping the type annotations before handing the code to V8. This is a significant shift: the runtime no longer requires a build step to run TypeScript, at least for the subset of syntax that consists only of type annotations.

Express, meanwhile, has its own TypeScript story. The framework is written in JavaScript, and its type definitions live in the DefinitelyTyped repository under @types/express . Express 5, released in October 2024, updated those types in ways that changed how req.params is typed, how wildcard routes behave, and how middleware signatures are checked . The combination of Node’s native TypeScript support and Express 5’s updated types is the foundation of modern TypeScript backend development.

Key point: Node.js 24 can run TypeScript files directly, but only for syntax that can be erased — type annotations, interfaces, type aliases. Features that generate JavaScript code, like enum or parameter properties, require either the --experimental-transform-types flag or a third-party tool like tsx . Type stripping performs no type checking. You still need tsc --noEmit in CI to catch type errors .


Why TypeScript on Node and Express

A REST API is a contract between the server and every client that calls it. The contract specifies the shape of request bodies, the types of route parameters, the structure of responses, and the errors that can be returned. In JavaScript, that contract exists only in documentation. In TypeScript, it exists in the compiler.

The request-body problem. A POST /users endpoint expects { email: string; name: string; password: string }. In JavaScript, the handler receives req.body as any. A typo in the handler — req.body.emial — compiles without complaint and fails at runtime when a real client sends a request. In TypeScript, req.body is typed, the typo is a compile error, and the editor autocompletes the correct field names .

The route-parameter problem. Express 5 changed how route parameters are typed. In Express 4, req.params.id was always string. In Express 5, if the route path contains a wildcard like /file/*path, the parameter is typed as string[]. If you declare the path explicitly, the type is inferred correctly. If you write a generic RequestHandler without a path, the type becomes string | string[], and any code that assumes string fails to compile .

The middleware problem. Express middleware chains pass req, res, and next through functions. Without types, it is easy to write middleware that returns a value where void is expected, or that calls next incorrectly. TypeScript’s RequestHandler type enforces the correct signature.

The validation problem. TypeScript types are erased at runtime. A handler typed to receive CreateUserDto will still receive whatever JSON the client sends — even if it is missing required fields or has extra ones. TypeScript does not validate incoming data. Libraries like Zod fill this gap: the schema is the source of truth, the TypeScript type is inferred from the schema, and the validation runs at runtime .

The trade-off. TypeScript adds a build step, or at least a type-checking step. Node.js can run the .ts file, but it will not tell you if the types are wrong. The tsc --noEmit command is still required to verify type correctness. For projects where the types are simple and the data is trusted, this overhead may not be worth it. For REST APIs that serve external clients, the contract enforcement is the point.


a. Setting Up TypeScript with Express

The setup for a TypeScript Express application depends on whether you want Node.js to run the .ts files directly or whether you want to compile to JavaScript first. The direct approach is simpler; the compiled approach is more compatible.

The direct approach uses Node.js’s built-in type stripping. No tsc is needed to run the code. The package.json declares "type": "module", the tsconfig.json sets "module": "nodenext", and the entry point is a .ts file .

// src/index.ts
import express, { Request, Response } from 'express';

const app = express();
app.use(express.json());

app.get('/health', (req: Request, res: Response) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

The tsconfig.json must include "allowImportingTsExtensions": true and "rewriteRelativeImportExtensions": true, because Node.js requires explicit .ts extensions in import statements . The "erasableSyntaxOnly": true option ensures that only syntax compatible with type stripping is allowed — it rejects enum, parameter properties, and other features that would require transformation .

The compiled approach uses tsc to emit JavaScript. The tsconfig.json sets "outDir": "./dist", the package.json scripts run tsc before node, and the compiled output is what gets deployed. This is the traditional approach, and it remains the most compatible with hosting environments that expect .js files .

A third option is tsx, which runs TypeScript directly and supports the full TypeScript feature set, including enum and decorators. It uses esbuild under the hood, so it is fast, but it does not perform type checking. The tsc --noEmit command is still needed to check types .

The @types/express package must be installed as a development dependency. For Express 5, the version is ^5.0.0. The @types/node package provides types for Node.js core modules like path, fs, and process .


b. Typing Routes, Request Bodies, and Responses

Express route handlers receive req and res objects. The req object has properties for the route parameters, query string, and body. The res object has methods for sending responses.

The Request type from Express is generic. Its parameters specify the types of params, the response body, the request body, and the query string, in that order .

import { Request, Response } from 'express';

interface CreateUserDto {
  email: string;
  name: string;
  password: string;
}

interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

// GET /users/:id
router.get('/:id', (req: Request<{ id: string }>, res: Response) => {
  const user = users.find(u => u.id === req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// POST /users
router.post('/', (req: Request<{}, {}, CreateUserDto>, res: Response) => {
  const { email, name, password } = req.body;
  const user: User = {
    id: String(users.length + 1),
    email,
    name,
    createdAt: new Date(),
  };
  users.push(user);
  res.status(201).json(user);
});

The Request<{ id: string }> generic tells TypeScript that req.params.id is a string. Without it, the type is string | string[] in Express 5, and parseInt(req.params.id) would produce a type error .

The Request<{}, {}, CreateUserDto> generic specifies that the request body is a CreateUserDto. The req.body property is typed accordingly, and the destructuring in the handler is checked against the interface.

The Response type is not generic in the same way. The res.json() method accepts any value, and the return type of a handler is void or Promise<void>. Returning a value from a handler is a type error in Express 5’s types .

For route handlers that are defined separately from the route configuration, the handler function can be typed with RequestHandler:

import { RequestHandler } from 'express';

const getUser: RequestHandler<{ id: string }> = (req, res) => {
  // req.params.id is string
  res.json({ id: req.params.id });
};

The RequestHandler type is the function signature that Express expects. Using it explicitly ensures that the handler matches the expected shape, which is important when middleware or route registration is involved.


c. Runtime Validation and Error Handling

TypeScript types are erased at runtime. A handler that declares req.body as CreateUserDto does not validate that the body actually matches the interface. A client can send {} or { email: 123 }, and the handler will receive it as typed, even though the runtime value does not conform.

Zod is the standard library for bridging this gap. It provides a schema that validates at runtime and infers a TypeScript type from the same definition .

import { z } from 'zod';

export const createUserSchema = z.object({
  body: z.object({
    email: z.string().email('Invalid email format'),
    name: z.string().min(2, 'Name must be at least 2 characters'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
  }),
});

export type CreateUserInput = z.infer<typeof createUserSchema>['body'];

The z.infer utility extracts the TypeScript type from the schema. The schema is the single source of truth: the validation logic and the type definition cannot drift apart.

The validation middleware runs the schema against the request and returns a 400 response if it fails:

import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';

export const validate = (schema: AnyZodObject) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.parseAsync({
        body: req.body,
        query: req.query,
        params: req.params,
      });
      next();
    } catch (error) {
      if (error instanceof ZodError) {
        return res.status(400).json({ errors: error.errors });
      }
      next(error);
    }
  };
};

The middleware is attached to routes that need validation:

router.post('/', validate(createUserSchema), (req: Request, res: Response) => {
  const { email, name, password } = req.body;
  // ...
});

The req.body is still typed as any in the handler because the middleware does not change the type of the Request object. The schema validates the runtime value, but the TypeScript type of req.body remains untyped unless the handler declares it. The pattern is to use the schema’s inferred type in the handler:

import { CreateUserInput } from '../schemas/user.schema';

router.post('/', validate(createUserSchema), (req: Request<{}, {}, CreateUserInput>, res: Response) => {
  // req.body is now CreateUserInput
});

Error handling middleware in Express has four parameters: err, req, res, and next. The err parameter is typed as Error or a custom error class. The middleware must be registered after all routes, because Express processes middleware in order .

import { Request, Response, NextFunction } from 'express';

export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
  ) {
    super(message);
    Object.setPrototypeOf(this, ApiError.prototype);
  }
}

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({ error: err.message });
  }
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
};

The next parameter must be present for Express to recognize the function as error-handling middleware, even if it is not used. TypeScript requires it in the signature, and the noUnusedParameters option in tsconfig.json would flag it — the convention is to prefix it with an underscore or configure the linter to ignore it.


Complete Example Session

This session builds a typed Express API with Zod validation, a custom error class, and error-handling middleware.

// ============================================
// PART 1: THE TYPED ROUTES
// ============================================

// src/routes/users.routes.ts
import { Router, Request, Response } from 'express';
import { z } from 'zod';

const router = Router();

interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

const users: User[] = [];

// GET /users
router.get('/', (req: Request, res: Response) => {
  res.json(users);
});

// GET /users/:id
router.get('/:id', (req: Request<{ id: string }>, res: Response) => {
  const user = users.find(u => u.id === req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

export default router;

// ============================================
// PART 2: THE ZOD SCHEMA
// ============================================

// src/schemas/user.schema.ts
import { z } from 'zod';

export const createUserSchema = z.object({
  body: z.object({
    email: z.string().email('Invalid email format'),
    name: z.string().min(2, 'Name must be at least 2 characters'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
  }),
});

export const updateUserSchema = z.object({
  body: z.object({
    email: z.string().email().optional(),
    name: z.string().min(2).optional(),
  }),
  params: z.object({
    id: z.string().uuid('Invalid user ID'),
  }),
});

export type CreateUserInput = z.infer<typeof createUserSchema>['body'];
export type UpdateUserInput = z.infer<typeof updateUserSchema>['body'];

// ============================================
// PART 3: THE VALIDATION MIDDLEWARE
// ============================================

// src/middleware/validate.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';

export const validate = (schema: AnyZodObject) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.parseAsync({
        body: req.body,
        query: req.query,
        params: req.params,
      });
      next();
    } catch (error) {
      if (error instanceof ZodError) {
        return res.status(400).json({
          success: false,
          errors: error.errors.map(e => ({
            path: e.path.join('.'),
            message: e.message,
          })),
        });
      }
      next(error);
    }
  };
};

// ============================================
// PART 4: THE TYPED POST HANDLER
// ============================================

// src/routes/users.routes.ts (continued)
import { validate } from '../middleware/validate.middleware';
import { createUserSchema, CreateUserInput } from '../schemas/user.schema';

router.post(
  '/',
  validate(createUserSchema),
  (req: Request<{}, {}, CreateUserInput>, res: Response) => {
    const { email, name, password } = req.body;
    const user: User = {
      id: String(users.length + 1),
      email,
      name,
      createdAt: new Date(),
    };
    users.push(user);
    res.status(201).json(user);
  },
);

// ============================================
// PART 5: THE CUSTOM ERROR CLASS
// ============================================

// src/types/error.types.ts
export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
  ) {
    super(message);
    Object.setPrototypeOf(this, ApiError.prototype);
  }
}

export class NotFoundError extends ApiError {
  constructor(message: string = 'Resource not found') {
    super(404, message);
  }
}

export class ValidationError extends ApiError {
  constructor(message: string = 'Validation failed') {
    super(400, message);
  }
}

// ============================================
// PART 6: THE ERROR HANDLING MIDDLEWARE
// ============================================

// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { ApiError } from '../types/error.types';

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction,
) => {
  if (err instanceof ApiError) {
    return res.status(err.statusCode).json({
      success: false,
      error: err.message,
    });
  }

  console.error('Unexpected error:', err);
  res.status(500).json({
    success: false,
    error: 'Internal server error',
  });
};

// ============================================
// PART 7: THE TYPED AUTH MIDDLEWARE
// ============================================

// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';

export interface AuthRequest extends Request {
  user?: { id: string; role: string };
}

export const requireAuth = (
  req: AuthRequest,
  res: Response,
  next: NextFunction,
) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = { id: '1', role: 'admin' };
  next();
};

// ============================================
// PART 8: THE MAIN APPLICATION
// ============================================

// src/index.ts
import express from 'express';
import usersRouter from './routes/users.routes';
import { errorHandler } from './middleware/error.middleware';

const app = express();
app.use(express.json());

app.use('/api/users', usersRouter);

// Error handler must be last
app.use(errorHandler);

const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// ============================================
// PART 9: THE TSCONFIG FOR NODE TYPE STRIPPING
// ============================================

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"]
}

// ============================================
// PART 10: THE PACKAGE SCRIPTS
// ============================================

// package.json
{
  "name": "express-ts-api",
  "type": "module",
  "scripts": {
    "dev": "node --watch src/index.ts",
    "start": "node src/index.ts",
    "type-check": "tsc --noEmit",
    "test": "vitest run"
  },
  "dependencies": {
    "express": "^5.0.0",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.0.0",
    "typescript": "^5.7.0",
    "vitest": "^2.0.0"
  }
}

The ten parts cover the typed routes, the Zod schema, the validation middleware, the typed POST handler, the custom error class, the error-handling middleware, the typed auth middleware, the main application, the tsconfig.json for Node type stripping, and the package.json scripts.


Quick Reference

The Express Types

TypePurposeGeneric Parameters
Request<P, ResBody, ReqBody, ReqQuery>Incoming requestP = params, ResBody = response body, ReqBody = request body, ReqQuery = query
Response<ResBody>Outgoing responseResBody = JSON response body type
NextFunctionCallback to next middleware—
RequestHandler<P, ResBody, ReqBody, ReqQuery>Full handler signatureSame as Request
ErrorRequestHandlerError-handling middleware—

The Express 5 Params Behavior

Route Pathreq.params Type
/user/:id{ id: string }
/file/*path{ path: string[] }
No path (generic)string | string[]
Explicit Request<{ id: string }>{ id: string }

The Node.js TypeScript Support

FeatureNode.js 24Requires
Type annotations✅ SupportedNothing
Interfaces✅ SupportedNothing
Type aliases✅ SupportedNothing
enum❌ Not by default--experimental-transform-types or tsx
Parameter properties❌ Not by default--experimental-transform-types or tsx
Decorators❌ Not supportedtsx
Type checking❌ Nevertsc --noEmit

The Validation Libraries

LibraryRuntimeType InferenceMiddleware
Zod✅z.infer<typeof schema>Custom or zod-express-middleware
express-validator✅ManualvalidationResult
Joi✅ManualCustom

Best Practices

✅ Do This:

// Type the request body generic
router.post('/', (req: Request<{}, {}, CreateUserDto>, res: Response) => { ... }); // ✅
// Type the route params generic
router.get('/:id', (req: Request<{ id: string }>, res: Response) => { ... }); // ✅
// Use Zod for runtime validation with inferred types
const schema = z.object({ body: z.object({ email: z.string().email() }) });
type Input = z.infer<typeof schema>['body']; // ✅
// Run tsc --noEmit in CI
"type-check": "tsc --noEmit" // ✅
// Use erasableSyntaxOnly for Node type stripping
"erasableSyntaxOnly": true // ✅

❌ Don’t Do This:

// Don't use any for request bodies
router.post('/', (req: Request, res: Response) => { const { email } = req.body as any; }); // ❌
// Don't forget the generic for params in Express 5
router.get('/:id', (req: Request, res: Response) => { parseInt(req.params.id); }); // ❌ string | string[]
// Don't assume TypeScript types validate at runtime
// A typed request body can still receive invalid JSON. // ❌
// Don't use enum with Node type stripping
enum Status { Active, Inactive } // ❌ requires --experimental-transform-types
// Don't rely on tsx for type checking
// tsx does not perform type checking. // ❌

Common Pitfalls

PitfallWhy It HappensFix
req.params.id is string | string[]Express 5 default type without explicit pathUse Request<{ id: string }>
Promise returned where void expectedExpress 5 types stricterMake handler async and return void
Runtime validation missingTypes erased at runtimeUse Zod schema and middleware
enum fails with type strippingNon-erasable syntaxUse const object or tsx
Import path failsMissing .ts extensionAdd .ts to import specifiers
Error handler not calledRegistered before routesRegister error handler last

Real-World Examples

1. Typed GET Handler

router.get('/:id', (req: Request<{ id: string }>, res: Response) => { ... });

2. Typed POST Handler

router.post('/', (req: Request<{}, {}, CreateUserDto>, res: Response) => { ... });

3. Zod Schema

const schema = z.object({ body: z.object({ email: z.string().email() }) });

4. Validation Middleware

export const validate = (schema: AnyZodObject) => async (req, res, next) => { ... };

5. Custom Error Class

export class ApiError extends Error { constructor(public statusCode: number, message: string) { super(message); } }

6. Error Handler

app.use((err: Error, req: Request, res: Response, next: NextFunction) => { ... });

7. Auth Middleware

export interface AuthRequest extends Request { user?: { id: string; role: string } }

8. Node Type Stripping

{ "compilerOptions": { "module": "nodenext", "erasableSyntaxOnly": true } }

9. Type Check Script

{ "type-check": "tsc --noEmit" }

10. RequestHandler Type

const handler: RequestHandler<{ id: string }> = (req, res) => { ... };

Visual

The TypeScript Request Pipeline

┌──────────────────────────────────────────────┐
│  TYPESCRIPT REQUEST PIPELINE                 │
│                                              │
│  Client sends POST /users                    │
│       │                                      │
│       ▼                                      │
│  express.json() parses body                  │
│       │                                      │
│       ▼                                      │
│  validate(createUserSchema)                  │
│       │                                      │
│       ├─ Valid → next()                      │
│       └─ Invalid → 400 response              │
│       │                                      │
│       ▼                                      │
│  Handler (req: Request<{},{},CreateUserDto>) │
│       │                                      │
│       ├─ TypeScript checks req.body.email    │
│       └─ Runtime value is validated          │
│                                              │
└──────────────────────────────────────────────┘

The Express 5 Params Typing

┌──────────────────────────────────────────────┐
│  EXPRESS 5 PARAMS                            │
│                                              │
│  Route: /user/:id                            │
│    req.params.id → string                    │
│                                              │
│  Route: /file/*path                          │
│    req.params.path → string[]                │
│                                              │
│  No explicit path:                           │
│    req.params.id → string | string[]         │
│                                              │
│  Explicit generic:                           │
│    Request<{ id: string }>                   │
│    req.params.id → string                    │
│                                              │
└──────────────────────────────────────────────┘

The Node.js Type Stripping Flow

┌──────────────────────────────────────────────┐
│  TYPE STRIPPING                              │
│                                              │
│  TypeScript source:                          │
│  const x: number = 1;                        │
│                                              │
│  After stripping:                            │
│  const x         = 1;                        │
│                                              │
│  The type annotation is replaced with        │
│  whitespace. No source map needed.           │
│                                              │
│  Type checking: NOT performed.               │
│  Use tsc --noEmit for type checking.         │
│                                              │
└──────────────────────────────────────────────┘

The Zod Validation Flow

┌──────────────────────────────────────────────┐
│  ZOD VALIDATION                              │
│                                              │
│  const schema = z.object({                   │
│    body: z.object({                          │
│      email: z.string().email()               │
│    })                                        │
│  });                                         │
│                                              │
│  type CreateUserInput =                      │
│    z.infer<typeof schema>['body'];           │
│                                              │
│  Runtime: schema.parseAsync(req.body)        │
│    └─ Returns validated data or throws       │
│                                              │
│  TypeScript: CreateUserInput                 │
│    └─ Inferred from the same schema          │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Node.js TypeScript supportBuilt-in type stripping, stable in v24.12
Type stripping limitationNo type checking, no enum, no parameter properties
Full TypeScript supportUse tsx for runtime, tsc --noEmit for checking
Express types@types/express ^5.0.0
Request genericRequest<P, ResBody, ReqBody, ReqQuery>
Express 5 paramsstring for explicit paths, string[] for wildcards
Runtime validationZod, express-validator, Joi
Zod type inferencez.infer<typeof schema>
Error handlerFour parameters, registered last
Type checkingtsc --noEmit in CI

Key takeaways:

  • Node.js 24 can run TypeScript files directly, but only for erasable syntax. Type annotations, interfaces, and type aliases work. enum, parameter properties, and decorators require --experimental-transform-types or a tool like tsx. Type stripping performs no type checking — tsc --noEmit is still required .
  • Express 5 changed how req.params is typed. Explicit paths like /user/:id infer { id: string }. Wildcard paths like /file/*path infer { path: string[] }. A generic Request without a path infers string | string[], which breaks code that assumes string .
  • The Request generic specifies the types of params, response body, request body, and query. Request<{ id: string }, {}, CreateUserDto> gives the handler typed access to route parameters and a typed request body .
  • TypeScript types are erased at runtime. A handler typed to receive CreateUserDto does not validate that the body matches. Zod schemas validate at runtime and infer the TypeScript type from the same definition, keeping validation and types in sync .
  • Error-handling middleware must have four parameters and be registered last. The err parameter is typed as Error or a custom subclass. The next parameter must be present for Express to recognize the function as error-handling middleware .
  • Use RequestHandler or Request generics explicitly for route handlers. Without types, req.body is any and req.params is string | string[] in Express 5. The generic parameters restore type safety.
  • The tsconfig.json for Node type stripping requires specific settings. "module": "nodenext", "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true, and "erasableSyntaxOnly": true ensure compatibility with Node’s built-in execution .

Remember: TypeScript on Node and Express is about enforcing the API contract at compile time while validating it at runtime. Node.js can run the .ts files, but it will not check the types. Express 5’s types are stricter about route parameters and middleware signatures. Zod bridges the gap between compile-time types and runtime data. Use the Request generics to type params and bodies. Use Zod schemas to validate what the client actually sends. Run tsc --noEmit in CI. The combination gives you the editor autocomplete and compile-time errors of TypeScript, plus the runtime safety that types alone cannot provide.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!