CLASS 16
Notes on the TypeScript source files that implement the Express API. They cover environment handling, the main server entry, the Express app factory, and the Todo module (routes, controller, validation schema).
Application Code Overview
This is a simple Todo API built with Express and TypeScript.
It uses an in‑memory array as a database, validates incoming data with Zod, and follows a modular structure.
1. Environment Configuration – env.ts
import {z} from "zod" const envSchema = z.object({ PORT: z.string().optional().default("8080") }) function createEnv(env: NodeJS.ProcessEnv){ const safeParseResult = envSchema.safeParse(env) if(!safeParseResult.success) throw new Error(safeParseResult.error.message) return safeParseResult.data; } export const env = createEnv(process.env)
Purpose:
Validates and provides typed access to environment variables.
- Zod schema – defines that
PORTis an optional string, defaulting to"8080".
z.string()ensures it is a string;.optional().default("8080")provides a fallback. createEnvfunction – callssafeParse(instead ofparse) to avoid throwing inside Zod; we manually throw with a clear error message if validation fails.envexport – a typed object ({ PORT: string }) that can be imported anywhere.
Usingprocess.envdirectly would be unsafe and untyped.
Why this pattern?
Centralises environment validation, fails fast if a required variable is missing, and gives you auto‑completion for env.PORT.
2. Entry Point – index.ts
import http from "node:http" import { env } from './env.js' import { createServerApplication } from "./app/index.js" async function main(){ try { const server = http.createServer(createServerApplication()) const PORT: number = env.PORT ? +env.PORT : 8080 server.listen(PORT , ()=>{ console.log(`Server is running on PORT: ${PORT}`); }) } catch (error) { throw error } } main()
Purpose:
Bootstraps the HTTP server.
- Imports the native
httpmodule and creates a server using our Express application (fromapp/index.js). - The
PORTis read from the validatedenvobject, converted to a number (+env.PORT). - The
server.listencall starts the server. - The
try/catchis somewhat redundant here (it just re‑throws), but it could be extended to handle startup errors gracefully.
Note:
Using http.createServer(app) is equivalent to app.listen(), but it gives you direct access to the http.Server instance if needed (e.g., for WebSocket integration).
3. Express App Factory – app/index.ts
import express from "express" import type { Application } from "express" import todoRouter from './todo/routes.js' export function createServerApplication():Application { const app = express() app.use(express.json()) app.use("/todos", todoRouter) return app }
Purpose:
Creates and configures the Express application.
express.json()– middleware to parse JSON request bodies.- Mounts the
todoRouterunder the/todospath. - Returns the configured
appinstance.
Why a factory function?
It allows the app to be created lazily and makes testing easier (you can create a fresh app for each test).
4. Todo Module – Validation Schema – VALIDATION/todo.schema.ts
import z from "zod" export const todoValidationSchema = z.object({ id: z.string().describe("ID of the Todo"), title: z.string().describe("title of todo"), description: z.string().optional().describe("Description of the todo"), isCompleted: z.boolean().describe('If the todo item is completed or not') }) export type Todo = z.infer<typeof todoValidationSchema>
Purpose:
Defines the shape of a Todo item and provides both runtime validation and a TypeScript type.
z.object({...})– describes the expected fields.id– required string.title– required string.description– optional string.isCompleted– required boolean.
.describe()– adds a description (useful for generating documentation or error messages).z.infer<...>– extracts the TypeScript type from the schema, giving you aTodotype that matches the validation rules.
Advantage:
One source of truth – the schema defines both validation and type. No need to maintain a separate interface (the commented‑out ITodo is unnecessary).
5. Todo Controller – todo/controller.ts
import type { Request, Response } from 'express' import { todoValidationSchema, type Todo } from "../VALIDATION/todo.schema.js" class TodoController { private _db: Todo[] constructor() { this._db = [] } public handleGetAllTodos(req: Request, res: Response ){ const todos = this._db; return res.json({todos}) } public async handleInsertTodo(req: Request, res:Response){ const rawBody = req.body; try { const validationResult = await todoValidationSchema.parseAsync(rawBody); this._db.push(validationResult) res.status(201).json({success: true, todo: validationResult}) } catch (error) { return res.status(500).json({error: "Validation error"}) } } } export default TodoController
Purpose:
Encapsulates the business logic for Todo operations.
_db– private in‑memory array (acts as a simple database).handleGetAllTodos– returns all todos as JSON.handleInsertTodo–- Takes the raw request body.
- Uses
parseAsyncto validate asynchronously (can handle more complex async validation if needed). - If valid, pushes the todo into
_dband responds with201 Created. - If validation fails, catches the error and returns a generic
500(this could be improved to return the specific validation error).
Important:
The controller uses async/await – even though the current validation is synchronous, parseAsync returns a promise, so the method is prepared for future async validators.
The try/catch currently masks the actual error; a better approach would be to send the error details to the client (e.g., res.status(400).json(error)).
6. Todo Routes – todo/routes.ts
import { Router } from "express"; import TodoController from "./controller.js"; const router = Router() const controller = new TodoController() router.get('/', controller.handleGetAllTodos.bind(controller)) // router.get("/:id") router.post("/", controller.handleInsertTodo.bind(controller)) // router.put(":id") // router.delete(":/id") export default router
Purpose:
Defines the endpoints for the Todo resource and wires them to the controller methods.
- Creates a new
Routerinstance. - Instantiates the
TodoController. - Binds the controller methods to the instance (
.bind(controller)) so thatthisinside the methods refers to the controller instance. - Mounts
GET /andPOST /(relative to/todos, because the app mounts this router at/todos).
Why .bind?
When passing a method as a callback, this would otherwise be lost. .bind(controller) ensures the method retains the correct this context.
How It All Fits Together
- Startup –
index.tsreads the validatedenv, creates an HTTP server with the Express app, and starts listening. - Request Flow
- A request to
POST /todoshits the Express app. express.json()parses the body.- The router directs it to
todoRouter.post('/'). - The router calls
controller.handleInsertTodo(withthisbound). - The controller validates the body using the Zod schema.
- If valid, it saves the todo in memory and responds.
- If invalid, it responds with an error.
- A request to
- Data Storage – Currently in‑memory (
_dbarray). Restarting the server clears all todos.
TypeScript Highlights
- Strict mode – all the
tsconfig.jsonstrict options are in effect, so the code must handleundefinedproperly. - Type imports – using
import typeforRequest,Response, andApplicationavoids including them in the compiled JavaScript. z.infer– derives theTodotype from the schema, ensuring type safety.NodeJS.ProcessEnv– the built‑in type forprocess.envis used inenv.ts.
Potential Improvements
- Error handling – Return more detailed validation errors from the controller instead of a generic 500.
- Database – Replace the in‑memory array with a real database.
- Logging – Add logging middleware (e.g.,
morgan). - Environment variables – Validate more variables (e.g.,
NODE_ENV). - ID generation – Currently the client must supply an
id; better to generate it on the server (e.g., usinguuid). - Async validation – The controller already uses
parseAsync, but the schema is synchronous; you could add custom async checks later.