NestJS is a server-side framework, that can be used to create an API.
NestJS is a javascript framework, I’d say it was more of a proper framework compared to something like Node and Express which would be a very simple way to create an API. You get a lot of functionality out of the box with NestJS, and the code is set up to be structured. It’s set up to use TDD and Typescript. However, NestJS is more difficult to use for the first time than a simple Node/Express API would be.
There are many different options for how to code an API. Alternatives would be Node/Express in the javascript universe, or something like Laravel in PHP, or a service such as Google’s Firebase. I’d used NestJS before and wanted to use it again.
Whereas normally you might have an automated deployment process with Docker, for testing or as a dev server you might just want a cheap instance of Lightsail to save money. You may also be new to NestJS and appreciate the process of getting it set up manually on an instance. In the AWS ecosystem, Lightsail will be a lot cheaper than an EC2 instance.
This is mainly about how to set up a NestJS project that you already have running locally, but I’ve also looked at the code for sending email and connecting to the database briefly for reference.
We’ll…
To get a quick and dirty API set up on Ubuntu 24.04 on AWS Lightsail, here’s a quick walkthough.
sudo apt-get updatesudo apt upgrade -ysudo apt autoremovesudo reboot
When you log back in you should see something like this…
Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-1016-aws x86_64)* Documentation: https://help.ubuntu.com* Management: https://landscape.canonical.com* Support: https://ubuntu.com/proSystem information as of Thu Sep 26 17:53:22 UTC 2024System load: 0.01Usage of /: 15.1% of 18.33GBMemory usage: 48%Swap usage: 0%Temperature: -273.1 CProcesses: 109Users logged in: 0IPv4 address for ens5: 172.26.13.208IPv6 address for ens5: 2a05:d01c:78d:5900:f364:2234:b4bc:3deb* Ubuntu Pro delivers the most comprehensive open source security andcompliance features.https://ubuntu.com/aws/proExpanded Security Maintenance for Applications is not enabled.0 updates can be applied immediately.Enable ESM Apps to receive additional future security updates.See https://ubuntu.com/esm or run: sudo pro statusLast login: Thu Sep 26 17:41:53 2024 from 52.94.39.60
Now, make the directory you want to store the API code in. If you have a NestJS project on your computer you can just FTP the files into the new directory.
mkdir apicd api
Ubuntu is up-to-date, the code is in place.
Now, we need to run the npm install
command, but first we’ll need to install NPM…
$ sudo apt install npm -y$ npm -v9.2.0$ sudo npm install -g npm@latest```$ npm -v9.2.0$ sudo reboot
$ npm -v10.8.3$ cd api$ npm install
This resulted in the process gradually getting slower and slower and eventually stopping…
$ npm install⠦Killed
To fix the error we had with NPM, a quick search and it might be that we don’t have any swap space, so let’s set that up.
How To Add Swap Space on Ubuntu 22.04
sudo swapon --show
No output shown.
$ free -htotal used free shared buff/cache availableMem: 416Mi 188Mi 174Mi 2.7Mi 84Mi 228MiSwap: 0B 0B 0B
$ df -hFilesystem Size Used Avail Use% Mounted on/dev/root 19G 3.1G 16G 17% /tmpfs 209M 0 209M 0% /dev/shmtmpfs 84M 880K 83M 2% /runtmpfs 5.0M 0 5.0M 0% /run/lockefivarfs 128K 3.8K 120K 4% /sys/firmware/efi/efivars/dev/nvme0n1p16 881M 133M 687M 17% /boot/dev/nvme0n1p15 105M 6.1M 99M 6% /boot/efitmpfs 42M 12K 42M 1% /run/user/1000
$ sudo fallocate -l 1G /swapfile$ ls -lh /swapfile-rw-r--r-- 1 root root 1.0G Sep 26 18:22 /swapfile
$ sudo chmod 600 /swapfile$ sudo mkswap /swapfileSetting up swapspace version 1, size = 1024 MiB (1073737728 bytes)no label, UUID=98ad9322-669f-4821-b3d1-38445da1a258
$ sudo swapon /swapfile$ sudo swapon --showNAME TYPE SIZE USED PRIO/swapfile file 1024M 0B -2
$ free -htotal used free shared buff/cache availableMem: 416Mi 189Mi 159Mi 2.7Mi 99Mi 227MiSwap: 1.0Gi 0B 1.0Gi
$ sudo cp /etc/fstab /etc/fstab.bak # Backup fstab incase anything goes wrong$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # Make the swap file permanent$ cat /proc/sys/vm/swappiness60 # 60 is ok for a desktop$ sudo sysctl vm.swappiness=10 # Set to 10 for a servervm.swappiness=10
Add this to the bottom of /etc/sysctl.conf…
vm.swappiness=10
$ cat /proc/sys/vm/vfs_cache_pressure100$ sudo sysctl vm.vfs_cache_pressure=50vm.vfs_cache_pressure = 50
Add this to the bottom of /etc/sysctl.conf…
vm.vfs_cache_pressure = 50
Now that we have some swap space, let’s try the npm install
again.
If the “node_modules” directory exists, delete it and start again…
rm -rf node_modules/npm install
npm warn deprecated @humanwhocodes/[email protected]: Use @eslint/object-schema insteadnpm warn deprecated @humanwhocodes/[email protected]: Use @eslint/config-array insteadnpm warn deprecated [email protected]: This module is not supported, and leaks memory. Do notuse it. Check out lru-cache if you want a good and tested way to coalesce async requestsby a key value, which is much more comprehensive and powerful.npm warn deprecated [email protected]: This package is no longer supported.npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supportednpm warn deprecated [email protected]: Rimraf versions prior to v4 are no longer supportednpm warn deprecated [email protected]: This package is no longer supported.npm warn deprecated [email protected]: Glob versions prior to v9 are no longer supportednpm warn deprecated [email protected]: Glob versions prior to v9 are no longer supportednpm warn deprecated [email protected]: Glob versions prior to v9 are no longer supportednpm warn deprecated [email protected]: Glob versions prior to v9 are no longer supportednpm warn deprecated [email protected]: This package is no longer supported.added 980 packages, and audited 981 packages in 11m153 packages are looking for fundingrun `npm fund` for details32 high severity vulnerabilitiesTo address issues that do not require attention, run:npm audit fixTo address all issues (including breaking changes), run:npm audit fix --forceRun `npm audit` for details.
That worked, let’s try building the application…
npm run build
> [email protected] build> nest build
All working so far, let’s see if we can get the app connected to the internet…
Nginx has to be installed, then configured…
sudo apt install nginxsudo nano /etc/nginx/sites-available/api.conf
server {listen 80;server_name api.website.com;client_max_body_size 50M;location / {proxy_pass http://127.0.0.1:3000/;proxy_set_header Access-Control-Allow-Origin *;}}
$ sudo unlink /etc/nginx/sites-enabled/default$ sudo ln -s /etc/nginx/sites-available/api.conf /etc/nginx/sites-enabled/$ sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful$ sudo systemctl enable nginx$ sudo service nginx restart
To get the API to be always running on port 3000 we can use PM2…
ecosystem.config.js…
module.exports = {apps: [{name: 'api',script: './dist/src/main.js',},],};
$ sudo npm install pm2@latest -g$ pm2 completion install$ pm2 -v5.4.2
Make ecosystem.config.js file in the root
pm2 startpm2 logspm2 plus #Sign up to view the web dashboard
If there are any problems pm2 logs
should tell you what they are.
main.ts…
const app = await NestFactory.create<NestExpressApplication>(AppModule, {cors: true,});
Also, in the Nginx config file we had…
proxy_set_header Access-Control-Allow-Origin *;
In this example I’m connecting to MongoDB, a service that I don’t need to do much setup to so I can get this example working pretty quickly.
I won’t go too much into how NestJS and TypeORM work with MongoDB, but the official docs are pretty good…
One thing to bear in mind is to whitelist the IP of the Lightsail instance for your MongoDB database. Trying to connect to a database but not being allowed to because the IP is not whitelisted will generally be a bad thing.
If you haven’t used NestJS before it can be quite daunting. I’m including some sample code to give an idea of how it works as even the official docs can be confusing.
To begin with, to test the connection and get some database calls happening pretty quickly I created a user.schema.ts like this, below. I am validating the email field in the Schema to make sure it is a valid email…
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';import { HydratedDocument } from 'mongoose';export type UserDocument = HydratedDocument<User>;const validateEmail = function (email: string): boolean {const re = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;return re.test(email);};@Schema()export class User {@Prop({type: String,required: [true, 'Username required'],validate: [validateEmail, 'username must be a valid email address'],unique: [true, 'email address already used, try again'],})username: string;@Prop({type: String,required: [true, 'Password required'],hide: true,})password: string;@Prop({ type: Date })created_at: Date;@Prop({ type: Date })last_logged_in_at: Date;}export const UserSchema = SchemaFactory.createForClass(User);
Then, my user.service.ts could be something like this…
import { Injectable, UnauthorizedException } from '@nestjs/common';import { InjectModel } from '@nestjs/mongoose';import { Model } from 'mongoose';import { User } from '../common/schemas/user.schema';@Injectable()export class UserService {constructor(@InjectModel(User.name) private readonly userModel: Model<User>,) {}async getAllUsers(): Promise<User[]> {return await this.userModel.find().exec();}async getUser(username: string): Promise<User> {return await this.userModel.findOne({ username: username });}}
I have the password getting checked in the Validation, for example. If all emails and passwords must be in a certain format, I will not even ask the database if they are in an incorrect format…
import {IsEmail,IsOptional,IsString,Matches,MinLength,} from 'class-validator';export class CreateUserFormDto {@IsEmail()username: string;@IsString()@MinLength(8)@Matches(/\d/, {message: 'password must contain a number',})@Matches(/[a-z]/, {message: 'password must contain a lowercase letter',})@Matches(/[A-Z]/, {message: 'password must contain an uppercase letter',})@Matches(/[`!@#$£%^&*()_+\-=\[\]{};':"\\|,\.<>\/?~]/, {message: 'password must contain a special character',})password: string;@IsOptional()created_at: Date;@IsOptional()last_logged_in_at: Date;}
Check the correct email connection info using swaks…
sudo apt-get install swaks
What we’re doing here is checking that the server, username (—au) and password (—ap) are correct before we try to connect in the app. In this case I am using my Mailgun info, but you should be able to use any email provider, although some may have a more complex log in system.
./swaks --auth \--server smtp.mailgun.org \--to [email protected] \--body 'This is a swaks test' \--h-Subject 'Swaks Test!' \--au [email protected] \--ap 'TheAPIKeyOrPassword'
Once you have the SMTP info that works, choose an email package to use with NestJs… As I am using Mailgun, I tried a couple of different packages before just plumping for a general email sending package…
npm install --save @nestjs-modules/mailer nodemailernpm install --save-dev @types/nodemailer
Then sending an email can be something like this, for example, email.service.ts…
import { Injectable } from '@nestjs/common';import { MailerService } from '@nestjs-modules/mailer';@Injectable()export class MailgunService {constructor(private readonly mailService: MailerService) {}async sendNewUserMail(username: string, subject: string, message: string) {await this.mailService.sendMail({from: '[email protected]',to: username,subject: subject,text: message,});}}
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';import { MongooseModule } from '@nestjs/mongoose';import { MailerModule } from '@nestjs-modules/mailer';import { JwtService } from '@nestjs/jwt';import { AppController } from './app.controller';import { AppService } from './app.service';import { UserModule } from './user/user.module';@Module({imports: [ConfigModule.forRoot({ isGlobal: true }),MongooseModule.forRoot(process.env.MONGOOSE_CONNECTION),MailerModule.forRoot({transport: {host: process.env.MAILGUN_DOMAIN,auth: {user: process.env.MAILGUN_USER,pass: process.env.MAILGUN_API_KEY,},},}),UserModule,],controllers: [AppController],providers: [AppService, JwtService],})export class AppModule {}
sudo npm install pm2@latest -g
rm -rf node_modulesrm package-lock.jsonnpm cache clean --forcenpm install
Upgrade NVM to the latest version…
$ nvm -v10$ nvm install 14
View PM2 logs to see any error messages or console.logs…
pm2 logscat /home/ubuntu/.pm2/logs/api-error.log
Or, if you’re just using node, you can cat or tail the error logs where you specified.
When first setting up NestJs for the first time, something that may not give helpful error messages might be if the database is not connected properly.
For example, I had to whitelist the API instance IP address with the database service for the database to work. Because the database was not connecting successfully I was unable to get the application working at all until the IP was whitelisted.
It’s also possible to run the API on port 3000 using node. To run the application and write to the access and error logs, instead of displaying the info on screen, like this…
node dist/main.js > api.log 2> api_error.log
All the usual methods for troubleshooting can apply. Try/catch can be particularly useful.
This has been an overview of some of the basic things to look at when initially setting up NestJS on an Ubuntu Lightsail instance. I’ve included a little bit of code the aid in understanding, but this is mainly looking at the general concepts. While we get the CORS working, connect to the database, and send email, there are plenty of other things to consider and that a fully functioning website will need. The code is to give anyone who is brand new to NestJS and wants to learn some of the concepts an idea to work from.
I have tried to link to other guides I’ve used here, but if I haven’t posted a link I probably have used another guide as a template and made several stackoverflow queries in each section.
Quick Links
Legal Stuff