10 Minutes from First Line of Code to Live Deployment: A Super Fast Nest.js Blog Course
Daniel Hayes
Full-Stack Engineer · Leapcell

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
Use the CLI to create a new project.
nest new personal-blog
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
@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.
After registering an account on the website, click "Create Database".
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.
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 ssl
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 {}
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
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; }
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 {}
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() );
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); } }
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
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();
In the project root directory (at the same level as src
), create views
and public
folders:
- personal-blog
- src
- views
- public
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>
-
_footer.ejs
(Reusable footer)</main> <footer> <p>© 2025 My Blog</p> </footer> </body> </html>
-
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') %>
-
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">← Back to Home</a> <%- include('_footer') %>
-
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') %>
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; }
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 }; } }
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'); } }
Run the Blog
Run the following command in your terminal to start the blog:
npm run start:dev
Open http://localhost:3000
in your browser to see the blog page. You can try writing a new post!
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.
Follow the steps below:
- 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.
- Click "Create Service" on the Leapcell page.
- After choosing your Nest.js repo, you'll see Leapcell has auto-populated the necessary configurations.
- 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.
Now, you can share this link with your friends, and everyone can see your blog online!