These notes are not intended as a comprehensive guide to the topic. Their purpose is to guide you through the main areas you should learn about, with resources provided for further exploration. The goal is for you to learn enough to complete the associated challenge.
You can think of Node.js as a way to use JavaScript for writing back-end servers, not just front-end websites
Further learning
node -v
npm -v
If you already have Node.js but it’s quite old, consider updating it using a version manager such as nvm
You can run plain JavaScript files directly with Node.js
hello.js
console.log('Hello from Node.js')
node hello.js
Hello from Node.js
This is useful for testing code, running small utilities, or learning JavaScript outside the browser
package.jsonWhen you create a Node.js project, you use a package manager to install external libraries (also known as dependencies). Node.js comes with npm by default, but you can also use yarn or pnpm for similar purposes.
mkdir node-demo
cd node-demo
npm init -y
This creates a package.json file which tracks dependencies and configuration
When you run npm init -y, a new file called package.json is created. It stores information about your project and dependencies
Example:
{
"name": "demo-app",
"version": "1.0.0",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {},
"devDependencies": {}
}
npm start or npm testYou can run scripts using:
npm run start
Run npm install <package> to add dependencies
Run npm uninstall <package> to remove them
Install a library:
npm install chalk
This adds chalk under dependencies in package.json and creates a node_modules folder where it is stored
Use it in your code:
import chalk from 'chalk'
console.log(chalk.green('Success'))
Development tools such as nodemon are usually added as dev dependencies:
npm install --save-dev nodemon
These are not included when the app is deployed to production.
package-lock.json fileWhen you install packages, npm automatically creates or updates package-lock.json.
This file records the exact versions installed so your project remains consistent across different machines.
npm update
npm uninstall chalk
node_modules to version control - it should be excluded using .gitignorenpm install when cloning a project - this installs all required dependenciesFurther learning
When developing, you don’t want to manually restart your app every time you change a file. Tools such as nodemon or ts-node-dev automatically restart the Node process when your code changes
npm install -g nodemon
or as a local dependency
npm install --save-dev nodemon
package.json
"scripts": {
"dev": "nodemon server.js"
}
npm run dev
Now, every time you save a file, nodemon will restart the server automatically
Further learning
Node.js supports two import styles:
CommonJS (older)
const express = require('express')
ES Modules (modern)
import express from 'express'
To use ES modules, set "type": "module" in your package.json. Most new projects now use this modern format
Node.js includes modules that let you read and write files directly on your computer
Example:
import fs from 'fs'
fs.writeFileSync('message.txt', 'Hello Node.js')
const text = fs.readFileSync('message.txt', 'utf8')
console.log(text)
This is the foundation for many tasks such as creating logs or configuration files
Further learning
Node.js can run any kind of script, not just web servers
Example: A script that prints today’s date
const now = new Date()
console.log(`The current date and time is: ${now.toISOString()}`)
Save as date.js and run it:
node date.js
You can write automation scripts like this to handle repetitive tasks such as copying files, resizing images, or generating reports
You can add external packages to extend your app
Example using chalk to add colour to console output:
npm install chalk
import chalk from 'chalk'
console.log(chalk.green('Success'))
console.log(chalk.red('Error'))
Further learning
Express is a framework for building web servers and APIs in Node.js. It handles routing, middleware, and responses
npm install express
server.js
import express from 'express'
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello from Express')
})
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`)
})
node server.js
Further learning
Routing defines how your server responds to different URLs and HTTP methods.
Each route represents a different path clients can request data from or send data to. For example, a front-end app might request /planets to get a list of planets, /planets/1 to get a specific planet, or send data to /planets to add a new one.
This separation of routes allows your API to handle multiple types of client requests in an organised way.
Example:
app.get('/planets', (req, res) => {
res.json([{ id: 1, name: 'Earth' }, { id: 2, name: 'Mars' }])
})
app.get('/planets/:id', (req, res) => {
const planetId = parseInt(req.params.id)
const planet = { id: planetId, name: 'Earth' }
res.json(planet)
})
app.post('/planets', (req, res) => {
const planet = req.body
res.status(201).json({ message: 'Planet created', planet })
})
Express can return plain text, HTML, or JSON objects. JSON is most common for APIs because it’s easy to use with JavaScript on the front-end.
Examples:
// Plain text
res.send('Hello from Express')
// JSON
res.json({ message: 'Data received successfully' })
// Custom status codes
res.status(404).json({ error: 'Planet not found' })
Further learning
Sometimes you’ll want to serve static files like images, CSS, or JavaScript directly from your server. Express provides a simple built-in middleware for this.
Example:
import express from 'express'
const app = express()
// Serve files from the 'public' directory
app.use(express.static('public'))
app.listen(3000, () => console.log('Server running on port 3000'))
Now, any file placed inside the public folder (for example public/index.html or public/styles.css) will be served automatically when requested by a client.
This is useful for serving documentation, static front-end assets, or uploads.
Further learning
When working with asynchronous code (e.g. database calls or external APIs), you should always use try/catch blocks to handle unexpected errors.
Example:
app.get('/data', async (req, res) => {
try {
const data = await fetchDataFromDatabase()
res.json(data)
} catch (error) {
console.error('Error fetching data:', error)
res.status(500).json({ error: 'Internal server error' })
}
})
If your server fails to handle an error, it can crash or leave clients hanging without a response. A 500 status code (Internal Server Error) signals to the client that something went wrong on the server side.
In production, instead of using console.error, errors should be logged properly to a logging service such as Sentry for later review.
Using error handling and logging ensures your API behaves predictably and safely even when things go wrong, and allows you to monitor issues.
Every route handler in Express receives two main objects: req (the request) and res (the response).
req contains information about the incoming request, such as query parameters, headers, and the body.res is used to send a response back to the client.Example:
app.get('/greet', (req, res) => {
const name = req.query.name || 'visitor'
res.send(`Hello, ${name}`)
})
Try visiting http://localhost:3000/greet?name=Alice and you’ll see:
Hello, Alice
Further learning
Express lets you define routes using different HTTP methods, which represent the type of operation the client is performing.
| Method | Purpose | Example |
|---|---|---|
| GET | Retrieve data | GET /planets |
| POST | Create new data | POST /planets |
| PUT | Replace existing data | PUT /planets/2 |
| PATCH | Update part of existing data | PATCH /planets/2 |
| DELETE | Remove data | DELETE /planets/2 |
Keeping these methods separate helps make your API clear and predictable.
Middleware are functions that run between receiving a request and sending a response. They are used for tasks such as parsing JSON, logging, authentication, and error handling
app.use(express.json())
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
})
Middleware runs in order, so put general-purpose ones (like logging) before routes
Further learning
By default, Express does not automatically read JSON bodies. You need to add a middleware to handle this.
app.use(express.json())
This allows you to access data sent in the body of a POST or PUT request via req.body.
Store settings and credentials outside your code, such as API keys and database URLs
npm install dotenv
.env file
PORT=4000
import dotenv from 'dotenv'
dotenv.config()
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Server running on port ${port}`))
Further learning
Cookies store small bits of data in the browser, such as session information
npm install cookie-parser
import cookieParser from 'cookie-parser'
app.use(cookieParser())
app.get('/set-cookie', (req, res) => {
res.cookie('theme', 'dark')
res.send('Cookie set')
})
app.get('/read-cookie', (req, res) => {
res.send(`Theme: ${req.cookies.theme}`)
})
Further learning
CORS allows your front-end app (running on a different port) to make API calls to your back-end
npm install cors
import cors from 'cors'
app.use(cors({ origin: 'http://localhost:5173' }))
Further learning
You can connect to databases with Node.js. The example below uses PostgreSQL:
pg
npm install pg
import pkg from 'pg'
const { Pool } = pkg
const pool = new Pool({
connectionString: process.env.DATABASE_URL
})
const result = await pool.query('SELECT NOW()')
console.log(result.rows)
Further learning
Express provides built-in tools for handling errors. Use middleware for consistency
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).json({ error: 'Something went wrong' })
})
For async handlers:
app.get('/example', async (req, res, next) => {
try {
const data = await someFunction()
res.json(data)
} catch (err) {
next(err)
}
})
Further learning
As your app grows, organise files for clarity
Example:
project/
│
├── server.js
├── routes/
│ └── planets.js
├── middleware/
│ └── logger.js
├── db/
│ └── index.js
├── .env
└── package.json
Keep each concern in its own folder
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
dotenv.config()
const app = express()
const port = process.env.PORT || 4000
app.use(cors())
app.use(express.json())
app.get('/', (req, res) => {
res.send('Welcome to the Space API')
})
app.get('/planets', (req, res) => {
res.json([
{ id: 1, name: 'Earth' },
{ id: 2, name: 'Mars' },
])
})
app.listen(port, () => console.log(`Server running on port ${port}`))
Run with:
npm run dev