Express with TypeScript
By Syncfusion®, Inc. · Last updated
Setting up tsx and tsc, catching bugs with tsc --noEmit, typing params, bodies and responses, extending Request with req.user, runtime type guards and Zod, and building for production.
What you will learn
- Set up an Express project with TypeScript
- Find bugs with tsc --noEmit
- Type params, bodies and responses
- Add req.user with declare global
- Validate input at runtime with type guards
Express with TypeScript
Many Express teams write their servers in TypeScript. It catches mistakes before the code runs.
- TypeScript = JavaScript plus types. A type says what shape a value has.
tsc= the TypeScript compiler. It checks types and turns.tsinto.js.tsx= a tool that runs.tsfiles directly, great for development.@types/express= ready-made types for Express, written by the community.- Types exist only while you write code. At runtime, it is all plain JavaScript.
Simple lesson: TypeScript checks your code before it runs. It cannot check what users send.
A Real-World Example
Think about the canteen order form.
- The form has fixed boxes: name, item, quantity. You cannot write a poem in the quantity box.
- The office checks the form before passing it on. Mistakes are caught early.
- But a student can still write "100" plates. Someone must check the real value too.
The fixed boxes are types. The office check is tsc. Checking the real value is runtime validation.
In this lesson, you will learn:
- How to set up an Express project with TypeScript.
- How
tsc --noEmitcatches mistakes. - How to type route params, bodies and responses.
- How to add your own fields, such as
req.user, to the Request type. - Why you still validate input at runtime.
- How to build for production.
Every lab in this course has a TypeScript version. Choose TypeScript in the Language menu in the top bar to see it in the other lessons. Fields that the app adds, such as req.user, live in a types.d.ts file there.
Setting Up
npm install express
npm install -D typescript tsx @types/express @types/node
npx tsc --init
A small tsconfig.json for Express on Node.js 22:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"]
}
strict: trueturns on the most useful checks. Always use it.- With
NodeNext, imports still end in.js, even though the file is.ts. npx tsx watch src/server.tsruns the app and restarts on changes.
import typebrings in only the type. It disappears from the JavaScript.req.params.idis always a string. TypeScript knows this, so you must convert it.
Simple lesson: Describe your data with interface, and let TypeScript check every use.
Catching Mistakes with tsc
tsx runs code without checking types. Run tsc --noEmit to check.
--noEmit= only check, do not write.jsfiles.- Add it as
"typecheck": "tsc --noEmit"inpackage.json, and run it in CI before every deploy. - Errors show the file, line, column and an error code like
TS2551.
item.priseis a typo forprice. In plain JavaScript, this givesNaNat runtime with no error.m.id === req.params.idcompares a number with a string. It would never match, so every lookup would fail.
The editors in these labs also understand TypeScript. Hover over req or item to see its type, and look for red underlines.
Simple lesson: Run tsc --noEmit before every commit. It finds bugs you would only see later.
Typing Params, Bodies and Responses
Request and Response take type arguments.
Request<Params, ResBody, ReqBody, Query>= the shape of each part.Response<Body>= whatres.json()is allowed to send.- Use
RequestHandleror a typed function for the whole handler. - Without types,
req.bodyisany. TypeScript then checks nothing.
Omit<Order, "id">makes a new type: an order without theid. No need to write it twice.- If a handler tries
res.json({ total: 5 }),tscreports that it is not anOrder.
Simple lesson: Give Request and Response your types, so every route is checked.
Adding req.user and Other Fields
Middleware often adds fields, such as req.user after login. TypeScript does not know about them.
- Add them to the global
Express.Requestinterface withdeclare global. - Put this in a file such as
src/express.d.ts. Every route then sees the new field. - Mark it optional (
user?:), because routes without login do not have it.
- No output means no errors. Try changing
"staff"to"teacher"inserver.ts, then run again.
Simple lesson: Teach TypeScript about fields your middleware adds with declare global.
Types Do Not Check Users
Types vanish when the code runs. A user can send anything.
req.body as NewOrderonly tells TypeScript to trust you. It checks nothing.- Always validate at runtime. See Input Processing and Validation.
- A type guard is a function that checks a value and tells TypeScript its type.
- Libraries such as Zod do both: check the value and create the type from one schema.
import { z } from "zod";
const NewOrder = z.object({ itemId: z.number().int(), quantity: z.number().int().min(1).max(10) });
type NewOrder = z.infer<typeof NewOrder>; // the type comes from the schema
const result = NewOrder.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error.flatten());
as is not a check. req.body as NewOrder is a promise you make to TypeScript, and users do not keep it.
Simple lesson: Types check your code. Validation checks your users. You need both.
Building for Production
- Development:
tsx watch src/server.ts. - Production: compile once with
tsc, then run plain JavaScript withnode. - Do not run
tsxin production. Compiling at startup is slower and hides type errors.
{
"scripts": {
"dev": "tsx watch --env-file-if-exists=.env src/server.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"start": "node dist/server.js",
"test": "node --test --import tsx"
}
}
- In Docker, run
npm run buildin the image, thenCMD ["node", "dist/server.js"]. See Deploying an Express App. - Node.js 22.18+ and 23.6+ can also run simple
.tsfiles directly by removing the types.tscis still needed for checking.
Easy way to remember: tsx to run, tsc to check, tsc to build, node to start.
Simple lesson: Check and compile with tsc. Ship plain JavaScript.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
Trusting req.body as Type | as checks the data | It only silences TypeScript | Bad input reaches your database |
Never running tsc | tsx checks types | tsx only removes them | Type errors reach production |
req.params.id used as a number | Params have the right type | Params are always strings | "2" === 2 is false, routes return 404 |
Importing ./types with no extension | TypeScript fixes imports | With NodeNext, write ./types.js | "Cannot find module" at runtime |
Using any everywhere | It is quicker | Use real types or unknown | TypeScript stops checking |
Adding req.user with no type | TypeScript will guess | Extend Express.Request with declare global | "Property user does not exist" |
Running tsx in production | It is the same as node | Build with tsc, start with node | Slower starts and hidden errors |
Check Yourself
Exercises
What to Remember
- TypeScript adds types to JavaScript. Install
typescript,tsx,@types/expressand@types/node. tsxruns.tsfiles.tsc --noEmitchecks them. Run the check in CI.- Type routes with
Request<Params, ResBody, ReqBody>andResponse<Body>. - Add fields like
req.userby extendingExpress.Requestwithdeclare global. - Types vanish at runtime. Validate input with type guards or Zod.
- Production:
tscbuilds todist,node dist/server.jsstarts it.
Next: Capstone: The Complete Canteen API. You will put every part of the course together in one project.