DEV Community

Cover image for 10 Minutes from First Line of Code to Live Deployment: A Super Fast Nest.js Blog Course
Leapcell
Leapcell

Posted on

10 Minutes from First Line of Code to Live Deployment: A Super Fast Nest.js Blog Course

Cover

This is a super fast course on Nest.js. In this tutorial, we will build a blog from the first line of code to deployment in just a few simple steps, taking less than 10 minutes.

The reason it's so fast is that this tutorial won't delve into detailed explanations of the process. Instead, it will guide you directly to build a finished product. I believe that learning a framework is faster by modifying an existing project to fit your own needs.

This blog consists of 3 functional modules, representing a common technology stack for a pure Node.js backend project:

  • Nest.js
  • PostgreSQL, as the database
  • ejs, for rendering pages

Without further ado, let's get started:

1. Initialize the Project

Nest.js projects rely heavily on the CLI tool for project management. First, let's install the Nest.js CLI.

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Use the CLI to create a new project.

nest new personal-blog
Enter fullscreen mode Exit fullscreen mode

This will create a new folder named personal-blog and install all the necessary dependencies. Open this directory in your favorite editor to officially start editing.

2. Connect to the PostgreSQL Database

Next, we will integrate a PostgreSQL database. Following the official recommendation, we'll use TypeORM as the ORM. The role of an ORM is to integrate the database into the code.

Install Dependencies

npm install @nestjs/typeorm typeorm pg
Enter fullscreen mode Exit fullscreen mode
  • @nestjs/typeorm: The official Nest.js module for TypeORM, adapting TypeORM for Nest.js.
  • typeorm: The TypeORM library itself.
  • pg: The Node.js driver for PostgreSQL, enabling Node.js to read and write to PostgreSQL.

Set up the Database

To speed up the tutorial, we will skip the step of installing and setting up a database locally. Instead, we'll provision an online database directly.

We can create a free database with one click on Leapcell.

Leapcell

After registering an account on the website, click "Create Database".

ImageP1

Enter a Database name, select a deployment region, and you can create the PostgreSQL database.

On the new page that appears, you will find the information needed to connect to the database. A control panel is provided at the bottom, allowing you to read and modify the database directly on the webpage.

ImageP2

Configure the Database Connection

Open the src/app.module.ts file and import TypeOrmModule.

Fill in the connection information using the database configuration from Leapcell. Note that sslMode must be set to true, otherwise, the connection will fail.

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'your_leapcell_host', // Replace with your Leapcell host
      port: 5432,
      username: 'your_postgres_username', // Replace with your PostgreSQL username
      password: 'your_postgres_password', // Replace with your PostgreSQL password
      database: 'personal_blog_db', // Replace with your database name
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true, // Set to true in development, it automatically syncs the database schema
      ssl: true, // Required for services like Leapcell
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Create the Posts Module

Next, let's create a module to manage blog posts.

You can use the Nest CLI to quickly generate the required files:

nest generate module posts
nest generate controller posts
nest generate service posts
Enter fullscreen mode Exit fullscreen mode

After that, we need to create the Post entity file to connect it with the database. In the src/posts directory, create a file named post.entity.ts:

// src/posts/post.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  title: string;

  @Column('text')
  content: string;

  @CreateDateColumn()
  createdAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to connect to the database.

Register TypeOrmModule in PostsModule: open src/posts/posts.module.ts and import TypeOrmModule.forFeature([Post]).

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { Post } from './post.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}
Enter fullscreen mode Exit fullscreen mode

Go to the Database details page on Leapcell and execute the following command in the web editor to generate the corresponding table.

CREATE TABLE "post" (
    "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    "title" VARCHAR NOT NULL,
    "content" TEXT NOT NULL,
    "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Finally, we need to create the PostsService. The PostsService will be responsible for handling all business logic related to posts. Open src/posts/posts.service.ts and add the following code:

// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './post.entity';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>
  ) {}

  findAll(): Promise<Post[]> {
    return this.postsRepository.find({
      order: {
        createdAt: 'DESC',
      },
    });
  }

  findOne(id: string): Promise<Post> {
    return this.postsRepository.findOneBy({ id });
  }

  async create(post: Partial<Post>): Promise<Post> {
    const newPost = this.postsRepository.create(post);
    return this.postsRepository.save(newPost);
  }
}
Enter fullscreen mode Exit fullscreen mode

Set Up EJS for Page Rendering

Now, we'll set up EJS so that we can render dynamic HTML pages.

Install Dependencies

npm install ejs
Enter fullscreen mode Exit fullscreen mode

Integrate the View Engine

Open the src/main.ts file and change it to the following:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';

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

  app.useStaticAssets(join(__dirname, '..', 'public'));
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('ejs');

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

In the project root directory (at the same level as src), create views and public folders:

- personal-blog
    - src
    - views
    - public
Enter fullscreen mode Exit fullscreen mode

Implement the Frontend Pages

Create the following files in the views folder:

  • _header.ejs (Reusable header)
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title><%= title %></title>
      <link rel="stylesheet" href="/css/style.css" />
    </head>
    <body>
      <header>
        <h1><a href="/">My Blog</a></h1>
        <a href="/posts/new" class="new-post-btn">New Post</a>
      </header>
      <main></main>
    </body>
  </html>
Enter fullscreen mode Exit fullscreen mode
  • _footer.ejs (Reusable footer)
      </main>
      <footer>
          <p>&copy; 2025 My Blog</p>
      </footer>
  </body>
  </html>
Enter fullscreen mode Exit fullscreen mode
  • index.ejs (Blog homepage)
  <%- include('_header', { title: 'Home' }) %>

  <div class="post-list">
    <% posts.forEach(post => { %>
    <article class="post-item">
      <h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
      <p><%= post.content.substring(0, 150) %>...</p>
      <small><%= new Date(post.createdAt).toLocaleDateString() %></small>
    </article>
    <% }) %>
  </div>

  <%- include('_footer') %>
Enter fullscreen mode Exit fullscreen mode
  • post.ejs (Post detail page)
  <%- include('_header', { title: post.title }) %>

  <article class="post-detail">
    <h1><%= post.title %></h1>
    <small><%= new Date(post.createdAt).toLocaleDateString() %></small>
    <div class="post-content"><%- post.content.replace(/\n/g, '<br />') %></div>
  </article>
  <a href="/" class="back-link">&larr; Back to Home</a>

  <%- include('_footer') %>
Enter fullscreen mode Exit fullscreen mode
  • new-post.ejs (New post page)
  <%- include('_header', { title: 'New Post' }) %>

  <form action="/posts" method="POST" class="post-form">
    <div class="form-group">
      <label for="title">Title</label>
      <input type="text" id="title" name="title" required />
    </div>
    <div class="form-group">
      <label for="content">Content</label>
      <textarea id="content" name="content" rows="10" required></textarea>
    </div>
    <button type="submit">Submit</button>
  </form>

  <%- include('_footer') %>
Enter fullscreen mode Exit fullscreen mode

2. Add CSS Styles

Add some simple styles in public/css/style.css:

/* public/css/style.css */
body {
  font-family: sans-serif;
  line-height: 1.6;
  margin: 0;
  background-color: #f4f4f4;
  color: #333;
}
header {
  background: #333;
  color: #fff;
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
header a {
  color: #fff;
  text-decoration: none;
}
main {
  max-width: 800px;
  margin: 2rem auto;
  padding: 1rem;
  background: #fff;
  border-radius: 5px;
}
.post-item {
  margin-bottom: 2rem;
  border-bottom: 1px solid #eee;
  padding-bottom: 1rem;
}
.post-item h2 a {
  text-decoration: none;
  color: #333;
}
.post-detail .post-content {
  margin-top: 1rem;
}
.new-post-btn {
  background: #5cb85c;
  padding: 0.5rem 1rem;
  border-radius: 5px;
}
.post-form .form-group {
  margin-bottom: 1rem;
}
.post-form label {
  display: block;
  margin-bottom: 0.5rem;
}
.post-form input,
.post-form textarea {
  width: 100%;
  padding: 0.5rem;
}
.post-form button {
  background: #337ab7;
  color: #fff;
  padding: 0.7rem 1.5rem;
  border: none;
  cursor: pointer;
}
footer p {
  text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

Connect the Backend with the Frontend Pages

Update src/posts/posts.controller.ts to handle HTTP requests and render EJS templates.

// src/posts/posts.controller.ts
import { Controller, Get, Render, Param, Post, Body, Res } from '@nestjs/common';
import { PostsService } from './posts.service';
import { Response } from 'express';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  @Render('index')
  async root() {
    const posts = await this.postsService.findAll();
    return { posts };
  }

  @Get('new')
  @Render('new-post')
  newPostForm() {
    return;
  }

  @Post()
  async create(@Body() body: { title: string; content: string }, @Res() res: Response) {
    await this.postsService.create(body);
    res.redirect('/posts');
  }

  @Get(':id')
  @Render('post')
  async post(@Param('id') id: string) {
    const post = await this.postsService.findOne(id);
    return { post };
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, update src/app.controller.ts to redirect the root path / to the blog homepage.

// src/app.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  root(@Res() res: Response) {
    return res.redirect('/posts');
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the Blog

Run the following command in your terminal to start the blog:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 in your browser to see the blog page. You can try writing a new post!

ImageP3

ImageP4

Now you can modify the website and API as you like, deepening your understanding of Nest.js in the process.

Deploy the Blog Online

Now you might be thinking, how can I show the website I made to others so that everyone can access it?

Remember Leapcell, which we used to create the database earlier? Leapcell can do more than just create databases; it's also a web app hosting platform that can host projects in various languages and frameworks, including Nest.js, of course.

Leapcell

Follow the steps below:

  1. Commit your project to GitHub. You can refer to GitHub's official documentation for the steps. Leapcell will pull the code from your GitHub repository later.
  2. Click "Create Service" on the Leapcell page. ImageP5
  3. After choosing your Nest.js repo, you'll see Leapcell has auto-populated the necessary configurations. ImageP6
  4. Click "Submit" at the bottom to deploy. The deployment will complete quickly and return you to the deployment homepage. Here we can see that Leapcell has provided a domain. This is the online address of your blog. ImageP7

Now, you can share this link with your friends, and everyone can see your blog online!


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)