Photo by Fotis Fotopoulos on Unsplash
How to Build a Restful API With NestJs, KnexJs, and ObjectionJs
Building a backend service that allows clients to make HTTP requests and get a response in nodejs and typescript can’t be better without nestjs because, without it, we will have to bootstrap everything ourselves, but with nestjs framework, all the hard work has been done for us, all we have to do is to write our business logic.
Nestjs is an opinionated nodejs framework built on top of node/typescript that allows you to build your backend service in record time and followed SOLID principles.
NestJS offers a level of abstraction above two very popular Node.js libraries, Express.js and Fastify. This means that all of the great middleware that is available for Express.js and Fastify can also be used with NestJS.
In this article, we are going to build a restful backend with Nestjs, knexJs, and objectionJs.
Knexjs is an SQL query builder for a relational database system, we can use it as a SQL query builder in both Node.JS and the browser, and its support all the popular relational database.
Objectionjs is ORM that let us embrace the full power of SQL Query, Objection.js is built on an SQL query builder called knexjs. community term it as a relational SQL query builder.
In this article we are going to use the power to nestjs, knexjs and objectionjs to build a resful service with relational database.
Getting Started.
To Get started, we need to install NestJs CLI, The Nest CLI is a command-line interface tool that makes it easy to develop, and maintain NestJS applications. It allows us to run our application in development mode, and to build and bundle it for a production-ready release.
npm i -g @nestjs/cli
Create a new Nestjs Project
nest new blogService
The above command will create a new nestjs project with the following project folder structure
app.controller.ts: A controller with a single route.
app.controller.spec.ts: The controller's unit tests.
app.module.ts: The root module of our application.
app.service.ts: A service for the AppModule's business logic.
main.ts: The entry file of our application.
Install required Dependencies
npm i @types/dotenv dotenv objection knex pg
npm install @nestjs/config @types/express
KnexJs Configuration
We will need to initialize our knexjs to enable us to create migration and run our SQL queries.
knex init -x ts
Let's configure our knexfile and setup knex command on package.json
import type { Knex } from 'knex';
// Update with your config settings.
const config: { [key: string]: Knex.Config } = {
stagging: {
client: 'sqlite3',
connection: {
filename: './dev.sqlite3',
},
},
development: {
client: 'postgresql',
connection: process.env.DATABASE_URL,
pool: {
min: 2,
max: 10,
},
migrations: {
tableName: 'knex_migrations',
},
},
In the knexfile, we will switch our stagging with development such that we can use the Postgres database in our development environment.
Create Migration Files
Since we are building a simple restful service that allows users to create a blog post, we will need two migration files, users and posts.
We will the following knex command to create two migrations, the command will create a migration folder and save our migration files.
knex migrate:make Posts -x ts
We will set up our post migration file, which will contain the following field.
import { Knex } from 'knex';
const tableName = 'posts';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable(tableName, (t) => {
t.increments('id');
t.text('title');
t.text('contents');
t.string('image');
t.integer('category_id');
t.integer('user_id');
t.timestamp('created_at').defaultTo(knex.fn.now());
t.timestamp('updated_at').defaultTo(knex.fn.now());
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable(tableName);
}
In order to run our migration and roll it back, we can use the following commands (ensure database connection is setup in knexfile)
knex migrate:latest
knex migrate:rollback
Database and ObjectionJs model setup
In order for knexjs to work with objectionjs, we need to create the models and connect them.
we will create a folder called db(you name it whatever fits you) and save our models and database provider setup.
The base model contains a read-only field that all other models will extend, thus ensuring they inherit Id.
import { Model } from 'objection';
export class BaseModel extends Model {
readonly id: number;
}
import { BaseModel } from './base.model';
export class PostModel extends BaseModel {
static tableName = 'posts';
title: string;
contents: string;
image: string;
categoryId: number;
userId: number;
createdAt: string;
updatedAt: string;
}
Each of our models can be used to run SQL Queries, we need to connect these model classes with knex database connection.
import { Global, Module } from '@nestjs/common';
import Knex from 'knex';
import { knexSnakeCaseMappers, Model } from 'objection';
import { PostModel } from './Models/Post.model';
const models = [PostModel];
const modelProviders = models.map((model) => {
return {
provide: model.name,
useValue: model,
};
});
const providers = [
...modelProviders,
{
provide: 'KnexConnection',
useFactory: async () => {
const knex = Knex({
client: 'pg',
connection: process.env.DATABASE_URL,
debug: process.env.KNEX_DEBUG === 'true',
...knexSnakeCaseMappers(),
});
Model.knex(knex);
return knex;
},
},
];
@Global()
@Module({
providers: [...providers],
exports: [...providers],
})
export class DatabaseModule {}
Once they are connected, we can expose those classes as injectable services to other modules via the exports array above.
The database module needs to be registered under the main Application module so that all its exported services are available to other modules.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './db/database.module';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), DatabaseModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Create Controllers and Provider service for Post Module
Create Controllers and Provider service for Post Module
We can either create the post resource [module, service, and controller] separately or we do them one after another.
We will create them one after another [module => controller => service]
nest generate mo Post , nest generate co Post, nest generate s Post
PostService.ts: Inside our post folder, we will have post.service.ts, hold our interaction with db via our SQL Queries.
We want to use Async/await to run our service function to execute our SQL Queries, then use data transfer objects (DTOs) to define the kind of data we will collect from the controller and use interface to define the kind of data our service will send back to the controller.
export class newPost {
title: string;
content: string;
image: string;
categoryId: number;
userId: number;
}
export class updatedPost {
title?: string;
content?: string;
image?: string;
categoryId?: number;
userId?: number;
}
export interface PostInterface {
id: number;
title: string;
contents: string;
image: string;
categoryId: number;
userId: number;
createdAt: string;
updatedAt: string;
}
Both newPost and updatedPost are DTOs, placed inside the dto folder, and PostInterface is placed inside the interface folder.
import { Injectable, Inject } from '@nestjs/common';
import { ModelClass } from 'objection';
import { PostModel } from 'src/db/Models/Post.model';
import { newPost } from './dto/newPost.dto';
import { updatedPost } from './dto/updatedPost.dto';
import { PostInterface } from './interface/post.interface';
@Injectable()
export class PostService {
constructor(@Inject('PostModel') private PostClass: ModelClass<PostModel>) {}
// fetch single post by id
async findPostById(postId: number): Promise<PostInterface> {
return await this.PostClass.query().findOne({ id: postId });
}
// fetch all post
async fetchAllPosts(): Promise<PostInterface[]> {
return await this.PostClass.query();
}
// create a post
async createPost(newPost: newPost): Promise<PostInterface> {
const savepost: PostInterface = await this.PostClass.query().insert(
newPost,
);
return savepost;
}
// update post
async updatePost(postId: number, updatedPost: updatedPost): Promise<boolean> {
await this.PostClass.query().update(updatedPost).where('id', postId);
return true;
}
// delete post
async deletePost(postId: number): Promise<boolean> {
await this.PostClass.query().deleteById(postId);
return true;
}
}
Each of the async functions above is defined by what they are doing.
PostController.ts: we are going to use the controller to handle HTTP requests from our endpoint to the right post service to handle the request, based on return values, then send a corresponding response to the client that made the request.
import { Body, Controller,Post, Get, Param, UnprocessableEntityException, ParseIntPipe,
Delete, Res, Patch,
} from '@nestjs/common';
import { PostService } from './post.service';
import { newPost } from './dto/newPost.dto';
import { PostInterface } from './interface/post.interface';
import { updatedPost } from './dto/updatedPost.dto';
import { Response } from 'express';
@Controller('posts')
export class PostController {
constructor(private postService: PostService) {}
@Get('')
async getAllPosts(@Res() response: Response) {
const allPost = await this.postService.fetchAllPosts();
return response
.status(200)
.send({ message: 'All Posts Available', data: allPost });
}
@Get('/:postId')
async getPostById(
@Param('postId', ParseIntPipe) postId: number,
@Res() response: Response,
) {
const post = await this.postService.findPostById(postId);
if (!post) {
throw new UnprocessableEntityException(
'Post with the provided Id Does Not Exist',
);
}
return response
.status(200)
.send({ message: 'Single Post Details', data: post });
// return post;
}
@Post('/new')
async createNewPost(@Body() PostDto: newPost, @Res() response: Response) {
try {
const newPost = await this.postService.createPost(PostDto);
return response
.status(201)
.send({ message: 'New Post Created', data: newPost });
} catch (error) {
throw new UnprocessableEntityException(error.message);
}
}
@Patch('/:postId/update')
async updatePost(
@Param('postId', ParseIntPipe) postId: number,
@Body() updatePostDto: updatedPost,
@Res() response: Response,
) {
try {
const updatePost = await this.postService.updatePost(
postId,
updatePostDto,
);
if (!updatePost) {
throw new UnprocessableEntityException(
'update operation cannot be completed at this time, try again',
);
}
} catch (error) {
throw new UnprocessableEntityException(error.message);
}
const updatedPost = await this.postService.findPostById(postId);
return response
.status(201)
.send({ message: 'Post Updated Successfully', data: updatedPost });
}
@Delete('/:postId')
async deletePostById(
@Param('postId', ParseIntPipe) postId: number,
@Res() response: Response,
) {
const post = await this.postService.findPostById(postId);
if (!post) {
throw new UnprocessableEntityException(
'Post with the provided Id Does Not Exist',
);
}
const deletePost = this.postService.deletePost(postId);
return response
.status(201)
.send({ message: 'Post Deleted Successfully', data: deletePost });
}
}
@Controller() decorator allows us to easily group a set of related routes, and minimize repetitive code.
just like in our own case where we want every route to the post controller to contain "posts"
@Controller("posts")
The @Get(), Post(), Patch(), and Delete()
HTTP request method decorator before the method tells Nest to create a handler for a specific endpoint for HTTP requests.
getAllPosts: this controller method uses the fetchAllPost method from the post service to fetch all posts from the DB using a simple query on top of objectionjs.
getPostById: findPostById method from the post service to use to handle this request, if the post exists, the right response is sent, else the not found error is sent.
createNewPost: we define the kind of data we are expecting from the request body via our PostDto class and pass the data to createPost method of post service. since we are creating a new resource in the DB, it is best advised, we use try/catch syntax, such that if the creation fails, we can catch the error and send it back to the client that made the request.
updatePost: using the updatePost Dto, we define the kind of data we want to accept from the request body, and we also accept the postId via route params, thus ensuring we are updating the right resource,
we use the postId to ensure we post we are about to update actually exist, try/catch syntax was used to handle error where update operation was not successful.
deletePost: using the postId provided via the route params, we ensure the post exists before actually calling the deletePost method of postService.
Before we run our test, need to add PostModule into the import array in the app.module, this way nest can perform the injection and resolve all its dependencies.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { PostModule } from './post/post.module';
import { DatabaseModule } from './db/database.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
DatabaseModule,
PostModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Run a test
npm run start:dev
Resources:
Knexjs Docs, ObjectionJs Docs, Nestjs Docs
Conclusion
Using the power nestjs, we can use knexjs and objectionjs to build a simple restful service, by writing pure SQL queries thus increasing the performance of the backend and avoid the abstraction provided by ORMs,
You can look at the GitHub repo and follow along if miss any from the explanation above.