Debian is one of the most popular and widely-used Linux distributions, renowned for its stability and security. Node.js has emerged as a very popular platform for building fast, scalable network applications using JavaScript on the server. In this comprehensive tutorial, we‘ll go over the process of getting Node.js up and running on a Debian 11 "Bullseye" server from start to finish, following best practices for production deployments.

An Introduction to Node.js

Before we dive into the installation and setup, let‘s briefly discuss what exactly Node.js is and why it has become so popular for web development.

Node.js is a runtime environment for JavaScript that allows developers to build server-side applications and networking tools using JS. This means we can use the same programming language on both the frontend and backend.

Node performs very well compared to other platforms like PHP, Ruby and Python because of its asynchronous, non-blocking I/O model and single-threaded event loop. All long-running I/O operations like reading from the filesystem or network are handled in a pool of worker threads, while JS execution happens on a single main thread. This allows Node.js to handle tens of thousands of concurrent connections with very low overhead.

The node package manager (NPM) contains over 1.5 million reusable modules covering practically every conceivable use case. This rich ecosystem of battle-tested open source modules along with Node‘s speed and scalability is why companies like Netflix, Uber, eBay and Paypal have adopted it.

Step 1 – Install Prerequisites

Node.js apps are written in JavaScript, but actually compiled to native machine code for performance. So we need some compiler toolchains installed before we can install Node itself.

Run the following commands to install the prerequisites:

sudo apt update
sudo apt install build-essential gcc g++ make

The build-essential package contains tools like GCC and Make which are needed to build most C/C++ software from source.

Step 2 – Install Node.js Using apt

The easiest way to install Node.js on Debian is by enabling the NodeSource repo and using the apt package manager:

curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs

This will add the official NodeSource repositories to your system containing up-to-date versions of Node.js and NPM, install the nodejs package containing them, and also install some necessary libraries used to compile native modules.

To check that Node.js has been installed properly and its version, run:

node -v
# v16.14.2 - will vary depending on latest LTS

npm -v 
# 8.5.0

Step 3 – Manage Node Versions Using a Version Manager

The nodejs package always points to the latest LTS release of Node.js. In production deployments, you often want to manage multiple versions of Node and switch between them easily. The nvm tool allows you to do just that:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
# Add nvm scripts to shell config: 
source ~/.bashrc

nvm install --lts # install latest LTS version 
nvm use 16 # switch Node version

nvm provides the nvm install, nvm use and nvm uninstall commands to manage multiple releases.

Step 4 – Understanding the Node.js Execution Model

Now that Node is installed, before we start building apps, let‘s understand Node‘s asynchronous programming paradigm and event loop which allows it to process so many concurrent connections.

When Node.js starts, it initializes the event loop along with a JavaScript stack and heap memory space, a V8 interpreter, libuv worker thread pool and bindings to low-level I/O operations.

Any I/O operation like accessing the network, files or databases is delegated to the libuv thread pool responsible for that operation‘s system calls. libuv queues tasks and callbacks between these threads and Node‘s single main execution thread coordinated by the event loop.

This allows long running I/O tasks to be run parallelly without blocking the JavaScript execution. Callbacks for I/O are queued by the event loop and executed asynchronously when the response is received.

So Node is optimal for I/O heavy use cases like servers handling thousands of user requests compared to CPU-intensive operations which should be delegated to other languages.

Step 5 – Project Scaffolding and npm Basics

Now let‘s scaffold out a simple Node.js project so we can look at how everything fits together:

mkdir my-node-app
cd my-node-app
npm init -y # generate package.json 

# install express - popular web framework
npm install express

touch index.js # entry point

The package.json file contains app dependencies and scripts. Node modules are reusable components – express here for example, provides MVC architecture for web apps.

By default Node will look for the entry point index.js file. Let‘s build a simple web server:

/index.js

const express = require(‘express‘) 

const app = express()

app.get(‘/‘, (req, res) => {
  res.send(‘Hello World!‘)
})

const port = 3000
app.listen(port, () => {
    console.log(`App listening on port ${port}!`)
})

The require() method imports dependencies. We create an Express app, define a route handler for the root URL / and make the app listen on port 3000.

To run the app:

node index.js 
# App listening on port 3000!

Visit http://your_server_ip:3000 and you should see the "Hello World!" response.

Step 6 – Core Node.js Modules

Beyond third-party modules like Express, Node also provides a set of core modules that expose OS and network functionality.

Let‘s look at some commonly used ones:

  • HTTP – low-level HTTP client and server implementation
  • FS – interacting with the filesystem – read, write, append, open, close etc.
  • Path – utilities for resolving file paths
  • OS – provide OS information like hostname, cores etc.
  • Events – implement pub/sub pattern for event-driven programming
  • child_process – spawn child processes and execute system commands
  • net – low-level TCP sockets, IPC communication

For example the built-in HTTP interface can create a server similar to what we did with Express:

const http = require(‘http‘)

const server = http.createServer((req, res) => {
  // request handler logic  
}) 

server.listen(3000)  

So in summary, Node core provides low-level networking and OS APIs while npm modules like Express abstract them for easier web development.

Step 7 – The Node.js REPL

Node comes bundled with the read-eval-print-loop (REPL) – an interactive shell that can evaluate JavaScriptexpressions:

node
> 2 + 5
7

> const m = ‘Hello‘ + ‘ ‘ + ‘World‘
undefined

> m 
‘Hello World‘

> .exit // quit REPL

The REPL is extremely useful for experimenting with Node APIs, testing snippets before adding to an app etc. without having to constantly re-run scripts.

Step 8 – Building an Express REST API

Let‘s now use Express to build the REST API backend for a simple note-taking app.

First modify index.js to this:

const express = require(‘express‘)
const app = express()  

app.use(express.json()) // parse JSON bodies

const notes = [
  {
    id: 1,
    title: ‘Wake up early‘,
    done: false
  },
  {
    id: 2,
    title: ‘Exercise‘,
    done: false
  }
]

// Read - GET /notes
app.get(‘/notes‘, (req, res) => {
  res.json(notes)
})

// Create - POST /notes  
app.post(‘/notes‘, (req, res) => {
  const note = req.body

  notes.push(note)

  res.status(201).json(note)
})

app.listen(3000)

We define an in-memory array of notes, implement GET and POST handlers, parse request bodies as JSON and send back responses.

Test the API using curl or an app like Postman to GET and POST notes. We can extend it by handling PUT, DELETE etc.

Step 9 – Connect to MongoDB Database

For persistent storage, let‘s save the notes to a MongoDB database.

First install MongoDB on your Debian server. Then:

npm install mongoose

touch models/Note.js

/models/Note.js

const mongoose = require(‘mongoose‘)

const NoteSchema = new mongoose.Schema({
  title: String,
  done: Boolean
})

module.exports = mongoose.model(‘Note‘, NoteSchema)  

This defines the data model using Mongoose ODM. Update index.js:

// ...above code

const mongoose = require(‘mongoose‘) 
mongoose.connect(‘mongodb://localhost:27017/notesdb‘) 

// on connection 
mongoose.connection.on(‘connected‘, () => {  
  console.log(‘Connected to DB‘)
})

// Note model
const Note = require(‘./models/Note‘) 

// REST API handler methods

// GET /notes
app.get(‘/notes‘, async (req, res) => {
  // query notes collection
  const notes = await Note.find() 
  res.json(notes)
})

app.post(‘/notes‘, async (req, res) => {
  // create new Note document
  const note = new Note(req.body)

  // save to DB
  const savedNote = await note.save()

  res.status(201).json(savedNote)  
})

// ...

We connect to MongoDB, get access to the Note model and perform DB queries to get and save notes inside route handlers.

Step 10 – Authentication and Security

No web app is complete without solid authentication and permissions. Let‘s implement user signup and login with encrypted passwords.

Install JWT for token-based auth and bcrypt for password hashing:

npm install jsonwebtoken bcrypt

/models/User.js

const mongoose = require(‘mongoose‘)
const bcrypt = require(‘bcryptjs‘) 

const UserSchema = new mongoose.Schema({
  username: String,
  passwordHash: String  
})

// hash pwd before saving 
UserSchema.pre(‘save‘, async function(next) {
  const user = this
  if (user.isModified(‘password‘)) {
    user.passwordHash = await bcrypt.hash(user.password, 8)
  }
  next()
})

module.exports = mongoose.model(‘User‘, UserSchema)

Here we automatically hash passwords before storing to DB.

Let‘s implement signup:

const jwt = require(‘jsonwebtoken‘) 

// ...

app.post(‘/users‘, async (req, res) => {
  // create a new user
  const user = new User(req.body)

  // save user to DB
  await user.save() 

  // generate JWT
  const token = jwt.sign({ userId: user._id }, ‘SECRETKEY‘)

  res.send({ token })  
})

When a new user signs up, we generate a JWT auth token unique to that user.

The login route:

app.post(‘/login‘, async (req, res) => {

  // auth user
  const user = await User.findOne({ username: req.body.username })

  const isValid = await bcrypt.compare(req.body.password, user.passwordHash)  

  if (isValid) {
    // return JWT if pwd match
    const token = jwt.sign({ userId: user._id }, ‘SECRETKEY‘)
    res.send({ token })
  } else {
    res.status(400).send(‘Invalid credentials‘)
  }

})

Here we verify the password by comparing it against the hashed version before sending back a token.

Similarly add JWT authentication middleware to private routes, logout etc.

Step 11 – Deployment with PM2

So far our app has only run on localhost. Let‘s learn how to deploy Node apps properly in production.

PM2 is an advanced process manager for Node apps with built-in load balancing, 0-downtime reloads, and rich monitoring.

To install:

npm install -g pm2

pm2 start index.js

This will daemonize our app, meaning it runs in background as a service. PM2 adds monitoring, log aggregation, clustering etc. Great for deploying even large, complex Node sites.

Some useful PM2 commands:

pm2 list # list managed apps
pm2 monit # live monitoring  
pm2 reload <app> # 0-downtime reload
pm2 log # show logs
pm2 stop <app>  

Step 12 – Handling and Debugging Errors

Node apps fail if there are uncaught exceptions. So robust error handling is critical.

Some best practices include:

Handle errors properly in route handlers:

app.get(‘/data‘, async (req, res) => {
  try {
    // db query could fail
    const data = await SomeModel.find() 
    res.json(data)
  } catch (error) {
    res.status(500).send(‘Error fetching data‘)  
  }
})

Global error handler as Express middleware:

app.use((error, req, res, next) => {
  console.log(error.message) 
  res.status(500).send(‘Internal Server Error‘)
})

Enable debug logs using a logger like winston:

const logger = require(‘winston‘)

logger.debug(‘Debug message‘) 

Use built-in debugger:

node inspect index.js

The node inspect tool allows setting breakpoints and stepping through code like debugger in the browser.

For frontend debugging – enable Inspector protocol in Chrome and debug full stack!

Step 13 – Other Important Considerations

Here are some other best practices worth implementing in Node apps:

  • Enable compression using Express middleware to decrease response payload sizes
  • Use Helmet module for app security – setting HTTP headers properly etc.
  • Implement rate limiting against DDoS / brute-force attacks using express-rate-limit
  • Parameterize configuration using environment variables and .env files
  • Separate Express route logic from controllers for cleaner code
  • Add Swagger docs for your REST APIs
  • Use clustering with PM2 for load-balancing across CPU cores
  • Follow semantic versioning for npm modules
  • Setup CI/CD pipeline for running tests, linting and automated deploys

And that‘s a wrap! We went from installing Node on Debian to built an entire functioning application backend with authentication, database and security.

Node.js is being adopted widely thanks to its scalability and constant innovation from the community. I hope you found this guide useful – happy building!

Similar Posts