Blog /

Deploy a Nest.js API with DigitalOcean's App Platform

September 07, 2021

Nest.js is a Node.js API framework that includes a variety of built in tools for building robust services quickly.

In this tutorial, I'll show you how you can create your own simple Nest.js API service to add, update, and delete Restaurant information. Afterwards, we'll see how quickly we can host it using DigitalOcean's App Platform.

Setup

First, let's install Nest globally.

npm install -g nest

Create a new Nest project using the global Nest CLI tool.

nest new nest-digitalocean-app

After running, you should now have a boilerplate Nest project.

Creating the "restaurants" endpoint

Now, we are going to create the /restaurants endpoint with the following structure:

nest-digitalocean-app/
├─ src/
   ├─ restaurants/
      ├─ dto/
         ├─ create-restaurant.dto.ts
         ├─ update-restaurant.dto.ts
      ├─ restaurants.controller.ts
      ├─ restaurants.module.ts
      ├─ restaurants.service.ts
   ├─ ...

If you are new to Nest, this may look a bit daunting. Let's break it down.

DTO

dto/ - A folder to contain our Data Transfer Objects. These files define the data structure of a request body, when creating and updating a restaurant record.

// src\restaurants\dto\create-restaurant.dto.ts

export class CreateRestaurantDto {
  name: string;
  description: string;
  rating: number;
}

Restaurants Controller

restaurants.controller.ts - The entry point for the /restaurnts endpoint. Defines the different HTTP methods and route structure.

// src\restaurants\restaurants.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { RestaurantsService } from './restaurants.service';
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';

@Controller('restaurants')
export class RestaurantsController {
  constructor(private readonly restaurantsService: RestaurantsService) {}

  @Post()
  create(@Body() createRestaurantDto: CreateRestaurantDto) {
    return this.restaurantsService.create(createRestaurantDto);
  }

  @Get()
  findAll() {
    return this.restaurantsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.restaurantsService.findOne(+id);
  }

  @Patch(':id')
  update(
    @Param('id') id: string,
    @Body() updateRestaurantDto: UpdateRestaurantDto,
  ) {
    return this.restaurantsService.update(+id, updateRestaurantDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.restaurantsService.remove(+id);
  }
}

Restaurants Service

restaurants.service.ts - Provides the business logic for the routes defined in restaurants.controller.ts.

import { Injectable } from '@nestjs/common';
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';

@Injectable()
export class RestaurantsService {
  constructor() {}

  async create(createRestaurantDto: CreateRestaurantDto) {
    // ...
  }

  async findAll() {
    // ...
  }

  async findOne(id: number) {
    // ...
  }

  async update(
    id: number,
    updateRestaurantDto: UpdateRestaurantDto,
  ) {
    // ...
  }

  async remove(id: number) {
    // ...
  }
}

Restaurants Module

restaurants.module.ts - Defines RestaurantsModule, which will be imported into app.module.ts. This will allow the Nest service to serve the /restaurants endpoint.

// src\restaurants\restaurants.module.ts

import { Module } from '@nestjs/common';
import { RestaurantsService } from './restaurants.service';
import { RestaurantsController } from './restaurants.controller';

@Module({
  controllers: [RestaurantsController],
  providers: [RestaurantsService],
})
export class RestaurantsModule {}

Connecting to a Postgres database

Of course, we need a way to persist the Restaurant data to a database of some sort.

Luckily, DigitalOcean's App Platform offers the ability to create a "Dev Database". Simply put, it is a small Postgres database that can be connected to one or more sources.

To enable our app to connect to the Dev Database, let's first create a configuration file in our project to pull environment variables that will be loaded by the DO App Platform.

// src/config/configuration.ts

export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT, 10),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    db: process.env.DB_DATABASE,
    
    // In order to ensure we can connect to the DigitalOcean managed
    // database, we need to ensure that we include the generated Root
    // CA for the database.
    ca: process.env.DB_CA_CERT,
  },
});

This configuration file can be imported via the ConfigModule.forRoot() method. We can then use Nest's `TypeOrmModule` to load in the Postgres config info:

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

import { ConfigModule, ConfigService } from '@nestjs/config';
import { RestaurantsModule } from './restaurants/restaurants.module';
import configuration from './config/configuration';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    // Load the configutation file we made
    ConfigModule.forRoot({
      load: [configuration],
    }),
    
    // Configure TypeORM to connect to the Postgres database
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get<string>('database.host'),
        port: configService.get<number>('database.port'),
        username: configService.get<string>('database.user'),
        password: configService.get<string>('database.password'),
        database: configService.get<string>('database.db'),
        ssl: {
          ca: configService.get<string>('database.ca'),
        },
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    
    // Let's import RestaurantsModule so that we serve the /restaurants endpoint
    RestaurantsModule, 
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

We can even grab the port number defined in the config file from the `main.ts` file.

// src\main.ts

import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const configService: ConfigService = app.get<ConfigService>('ConfigService');

  const port: number = configService.get<number>('port');
  await app.listen(port);
}
bootstrap();

Using TypeORM to persist data to Postgres

Now let's create a TypeORM entity to define how the data needs to be saved to the Postgres database.

// src\restaurants\restaurant.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Restaurant {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  description: string;

  @Column()
  rating: number;
}

Now, we can update app.module.ts to include the new Restaurant entity.

// src\app.module.ts

import { Restaurant } from './restaurants/restaurant.entity';

...

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    type: 'postgres',
    host: configService.get<string>('database.host'),
    port: configService.get<number>('database.port'),
    username: configService.get<string>('database.user'),
    password: configService.get<string>('database.password'),
    database: configService.get<string>('database.db'),
    ssl: {
      ca: configService.get<string>('database.ca'),
    },
    
    // Load the imported entity from above
    entities: [Restaurant],
    
    synchronize: true,
  }),
  inject: [ConfigService],
}),

Finally, let's update the restaurants.service.ts file to find, add, update, and delete Restaurant records in the Postgres database.

import { Injectable } from '@nestjs/common';
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';
import { Restaurant } from './restaurant.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class RestaurantsService {
  constructor(
    @InjectRepository(Restaurant)
    private restaurantRepository: Repository<Restaurant>,
  ) {}

  async create(createRestaurantDto: CreateRestaurantDto): Promise<Restaurant> {
    const restaurant = this.restaurantRepository.create(createRestaurantDto);

    return this.restaurantRepository.save(restaurant);
  }

  async findAll(): Promise<Restaurant[]> {
    return this.restaurantRepository.find();
  }

  async findOne(id: number): Promise<Restaurant> {
    return this.restaurantRepository.findOne(id);
  }

  async update(
    id: number,
    updateRestaurantDto: UpdateRestaurantDto,
  ): Promise<Restaurant> {
    const updateResult = await this.restaurantRepository.update(
      id,
      updateRestaurantDto,
    );

    if (updateResult.affected > 0) {
      return this.findOne(id);
    }
  }

  async remove(id: number) {
    const restaurant = await this.restaurantRepository.findOne(id);
    if (restaurant) {
      this.restaurantRepository.delete(id);
    }
  }
}

The full Nest API example GitHub repo can be found here.

Deploying the API

Now we are ready to deploy our app!

If you have a GitHub account, you can easily connect it to the DigitalOcean App Platform. Then, simply select which repo you want to use to create and launch the app.

From here, you can configure your app's HTTP routes, environment variables, run command, etc. following DigitalOcean's App Platform How-To.

However, if you would like to quickly spin up this example to try it out, you can click the the "Deploy to DigitalOcean" button below.

Deploy to DO

Warning: Deploying the application may result in charges against your account.

Comments