mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
docs(api): add docs to initialize an API project
This commit is contained in:
parent
8308122de9
commit
191cc9b73d
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--runInBand"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}\\start",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
713
docs/curd.md
Normal file
713
docs/curd.md
Normal file
@ -0,0 +1,713 @@
|
||||
# Building RESTful APIs with NestJS
|
||||
|
||||
|
||||
|
||||
As described in the [Nestjs](https://nestjs.com) website, Nestjs is *a progressive Node.js framework for building efficient, reliable and scalable server-side applications.*
|
||||
|
||||
Nestjs combines the best programming practice and the excellent technologies from the communities.
|
||||
|
||||
* A lot of NestJS concepts are heavily inspired by the effort of the popular frameworks in the world, esp [Angular](https://www.angular.io), [Spring WebMVC](http://www.spring.io).
|
||||
* Nestjs is flexbile, you are free to choose [Expressjs](https://expressjs.com/) or [Fastify](https://www.fastify.io) as the background engine.
|
||||
* Nestjs provides a lot of third party project integration, from database operations, such as Mongoose, TypeORM, etc. to a Message Broker, such as RabbitMQ etc.
|
||||
|
||||
If you are new to Nestjs like me but has some experience of Angular or Spring WebMVC, kickstarting a Nestjs project is really not difficult.
|
||||
|
||||
## Generating a Nestjs project
|
||||
|
||||
Make sure you have installed the latest [Nodejs](https://nodejs.org/en/).
|
||||
|
||||
```bash
|
||||
npm i -g @nestjs/cli
|
||||
```
|
||||
|
||||
When it is finished, there is a `nest` command available in the `Path`. The usage of `nest` is similar with `ng` (Angular CLI), type `nest --help` in the terminal to list help for all commands.
|
||||
|
||||
```bash
|
||||
❯ nest --help
|
||||
Usage: nest <command> [options]
|
||||
|
||||
Options:
|
||||
-v, --version Output the current version.
|
||||
-h, --help Output usage information.
|
||||
|
||||
Commands:
|
||||
new|n [options] [name] Generate Nest application.
|
||||
build [options] [app] Build Nest application.
|
||||
start [options] [app] Run Nest application.
|
||||
info|i Display Nest project details.
|
||||
update|u [options] Update Nest dependencies.
|
||||
add [options] <library> Adds support for an external library to your project.
|
||||
generate|g [options] <schematic> [name] [path] Generate a Nest element.
|
||||
Available schematics:
|
||||
┌───────────────┬─────────────┐
|
||||
│ name │ alias │
|
||||
│ application │ application │
|
||||
│ class │ cl │
|
||||
│ configuration │ config │
|
||||
│ controller │ co │
|
||||
│ decorator │ d │
|
||||
│ filter │ f │
|
||||
│ gateway │ ga │
|
||||
│ guard │ gu │
|
||||
│ interceptor │ in │
|
||||
│ interface │ interface │
|
||||
│ middleware │ mi │
|
||||
│ module │ mo │
|
||||
│ pipe │ pi │
|
||||
│ provider │ pr │
|
||||
│ resolver │ r │
|
||||
│ service │ s │
|
||||
│ library │ lib │
|
||||
│ sub-app │ app │
|
||||
└───────────────┴─────────────┘
|
||||
```
|
||||
|
||||
Now generate a Nestjs project via:
|
||||
|
||||
```bash
|
||||
nest new nestjs-sample
|
||||
```
|
||||
|
||||
Open it in your favorite IDEs, such as [Intellij WebStorm](https://www.jetbrains.com/webstorm/) or [VSCode](https://code.visualstudio.com/).
|
||||
|
||||
## Exploring the project files
|
||||
|
||||
Expand the project root, you will see the following like tree nodes.
|
||||
|
||||
```bash
|
||||
.
|
||||
├── LICENSE
|
||||
├── nest-cli.json
|
||||
├── package.json
|
||||
├── package-lock.json
|
||||
├── README.md
|
||||
├── src
|
||||
│ ├── app.controller.spec.ts
|
||||
│ ├── app.controller.ts
|
||||
│ ├── app.module.ts
|
||||
│ ├── app.service.ts
|
||||
│ └── main.ts
|
||||
├── test
|
||||
│ ├── app.e2e-spec.ts
|
||||
│ └── jest-e2e.json
|
||||
├── tsconfig.build.json
|
||||
└── tsconfig.json
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
The default structure of this project is very similar with the one generated by Angular CLI.
|
||||
|
||||
* *src/main.ts* is the entry file of this application.
|
||||
* *src/app\** is the top level component in a nest application.
|
||||
* There is an a*pp.module.ts* is a Nestjs `Module` which is similar with Angular `NgModule`, and used to organize codes in logic view.
|
||||
* The *app.service.ts* is an `@Injectable` component, similar with the service in Angular or Spring's Service, it is used for handling business logic. A service is annotated with `@Injectable`.
|
||||
* The *app.controller.ts* is the controller of MVC, to handle incoming request, and responds the handled result back to client.
|
||||
* The *app.controller.spec.ts* is test file for *app.controller.ts*. Nestjs uses [Jest](https://jestjs.io/) as testing framework.
|
||||
* *test* folder is for storing e2e test files.
|
||||
|
||||
## Defining the APIs
|
||||
|
||||
I will reuse the concept I've used in the former examples - the blogging posts.
|
||||
|
||||
* GET /posts - Get all posts
|
||||
* GET /posts/id - Get post by id
|
||||
* POST /posts - Save a post
|
||||
* PUT /posts/id - Update the certain post specified by id
|
||||
* DELETE /posts/id - Delete a post
|
||||
|
||||
In the next steps, we will create:
|
||||
|
||||
* A new module `post` to organize all the features.
|
||||
* A `Post` interface to present the `Post` entity resource.
|
||||
* A `PostService` component to serve the data for controller. In this post, we use dummy data storage temporarily, and replace it with a real Mongo in the future post.
|
||||
* A `PostController` to expose APIs which is responsible for handling incoming requests.
|
||||
|
||||
|
||||
## Creating PostService
|
||||
|
||||
Following the Nestjs coding style, first of all, let's generate a module named `post`:
|
||||
|
||||
```bash
|
||||
nest g mo post
|
||||
```
|
||||
|
||||
`PosModule` is imported into the top-level `AppModule`. Check the content of file *src/app/app.module.ts*:
|
||||
|
||||
```typescript
|
||||
//... other imports
|
||||
import { PostModule } from './post/post.module';
|
||||
|
||||
@Module({
|
||||
imports: [PostModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
Then generate the `PostService`, `Post` respectively.
|
||||
|
||||
```bash
|
||||
nest g s post
|
||||
nest g interface post
|
||||
```
|
||||
The `PostService` is added to `PostModule` automatcially when it is generated.
|
||||
|
||||
```typescript
|
||||
//...other imports
|
||||
import { PostService } from './post.service';
|
||||
|
||||
@Module({
|
||||
providers: [PostService]
|
||||
})
|
||||
export class PostModule {}
|
||||
```
|
||||
|
||||
After it is done there are 4 files created in the *src/app/post* folder, including two specs files for testing `PostController` and `PostService`.
|
||||
|
||||
```bash
|
||||
post.interface.ts
|
||||
post.module.ts
|
||||
post.service.spec.ts
|
||||
post.service.ts
|
||||
```
|
||||
|
||||
Firstly, let's open `post.interface.ts` to model the `Post` entity.
|
||||
|
||||
```typescript
|
||||
export interface Post {
|
||||
id?: number;
|
||||
title:string;
|
||||
content: string;
|
||||
createdAt?: Date,
|
||||
updatedAt?: Date
|
||||
}
|
||||
```
|
||||
|
||||
In the above interface, add some fields as you see, here we make the `id`, `createdAt`, `updatedAt` are optional now.
|
||||
|
||||
Let's create a method in `PostService` to fetch all posts.
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
private posts: Post[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Generate a NestJS project',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Create CRUD RESTful APIs',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Connect to MongoDB',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
findAll(): Observable<Post> {
|
||||
return from(this.posts);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the codes, we used an array `posts` for the background data storage. The `findAll` method returns a `Observerable` which is created from the dummy `posts` we have declared.
|
||||
|
||||
Add a test for this method. Open `post.service.spec.ts`, add a new test case.
|
||||
|
||||
```typescript
|
||||
|
||||
describe('PostService', () => {
|
||||
let service: PostService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PostService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PostService>(PostService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('getAllPosts should return 3 posts', done => {
|
||||
service
|
||||
.findAll()
|
||||
.pipe(take(3), toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(3),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In the `beforeEach` , it prepares a `TestingModule` to assemble resources in the test.
|
||||
|
||||
> Like the annotations (@BeforeEach, etc.) provided JUnit 5 in Java world, there are some similar hooks ready for preparing the test and doing clean work in a Jest test, such as `beforeAll, afterAll, beforeEache, afterEach` , etc.
|
||||
|
||||
In the newly added `it('getAllPosts should return 3 posts', done => {}` , it uses a `done` handler to mark the an asynchronous task is done. It is very helpful to test `Promise` or `Observerable` invocation in a test.
|
||||
|
||||
The `pipe` accepts a series of Rxjs operators for some `processor` working, such as filtering, transforming, collecting, etc. check the [Rxjs official docs](https://rxjs.dev/operator-decision-tree).
|
||||
|
||||
The `subscribe` accept a subscriber to handle the data stream, see the [Subscriber API](https://rxjs.dev/api/index/class/Subscriber) for more details.
|
||||
|
||||
Run `npm run test` command in the root folder to run all tests.
|
||||
|
||||
```bash
|
||||
❯ npm run test
|
||||
|
||||
> nestjs-sample@0.0.1 test D:\hantsylabs\nestjs-sample
|
||||
> jest
|
||||
|
||||
...
|
||||
Time: 4.563 s, estimated 11 s
|
||||
Ran all test suites.
|
||||
```
|
||||
|
||||
|
||||
|
||||
> If you want to track the changes on test codes and rerun the test cases automatically, use `npm run test:watch` instead.
|
||||
|
||||
It works.
|
||||
|
||||
Let's modify the `findAll` slightly, make it accept a keyword to query the posts.
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
//...
|
||||
findAll(keyword?: string): Observable<Post> {
|
||||
if (keyword) {
|
||||
return from(this.posts.filter(post => post.title.indexOf(keyword) >= 0));
|
||||
}
|
||||
|
||||
return from(this.posts);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add another test to verify filtering the posts by keyword.
|
||||
|
||||
```typescript
|
||||
it('getAllPosts with keyword should return 1 post', done => {
|
||||
service
|
||||
.findAll('Generate')
|
||||
.pipe(take(3), toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(1),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
When using a keyword *Generate*, only one item will be included in the data stream. Run the test again, it will pass.
|
||||
|
||||
Let's move the next one. Create a `findById` method in `PostService` class. When a post is found return the found post else return a Rxjs `EMPTY`.
|
||||
|
||||
```typescript
|
||||
findById(id: number): Observable<Post> {
|
||||
const found = this.posts.find(post => post.id === id);
|
||||
if (found) {
|
||||
return of(found);
|
||||
}
|
||||
return EMPTY;
|
||||
}
|
||||
```
|
||||
|
||||
And add a test for the case when a post is found.
|
||||
|
||||
```typescript
|
||||
it('getPostById with existing id should return 1 post', done => {
|
||||
service.findById(1).subscribe({
|
||||
next: data => {
|
||||
expect(data.id).toBe(1);
|
||||
expect(data.title).toEqual('Generate a NestJS project');
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
When the `findById` return a `EMPTY`, I tried to use the same approach to verify the result in the `next` handler, but failed, the `next` is never called, I started a [topic on stackoverflow](https://stackoverflow.com/questions/62208107/how-test-a-rxjs-empty-operater-using-jest), and got two solutions for handling the `EMPTY` stream.
|
||||
|
||||
The first solution is utilizing the *testing* facility from Rxjs. Set up a `TestScheduler` for test, and use `expectObservable` to assert the data stream in [marble diagrams](https://rxjs-dev.firebaseapp.com/guide/testing/internal-marble-tests).
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
testScheduler = new TestScheduler((actual, expected) => {
|
||||
// asserting the two objects are equal
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
// This test will actually run *synchronously*
|
||||
it('test complete for empty()', () => {
|
||||
testScheduler.run(helpers => {
|
||||
const { cold, expectObservable, expectSubscriptions } = helpers;
|
||||
expectObservable(service.findById(10001)).toBe('|');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The second test is using a signal myself to make sure it is completed.
|
||||
|
||||
```typescript
|
||||
it('getPostById with none existing id should return empty', done => {
|
||||
let called = false;
|
||||
service.findById(10001).subscribe({
|
||||
next: data => {
|
||||
console.log(data);
|
||||
called = true;
|
||||
},
|
||||
error: error => {
|
||||
console.log(error);
|
||||
called = true;
|
||||
},
|
||||
complete: () => {
|
||||
expect(called).toBeFalsy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Another workaround is converting the `EMPTY` stream to an array and asserting it is an empty array in the `next` handler.
|
||||
|
||||
```typescript
|
||||
it('getPostById with none existing id should return empty', done => {
|
||||
service
|
||||
.findById(10001)
|
||||
.pipe(toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(0),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Ok, we have resolved the `emtpy` stream testing issue. Go ahead.
|
||||
|
||||
Next, let's create a method to save a new `Post` into the data storage. Since we are using an array for the dummy storage, it is simple when saving a new `Post`, just append it into the end of the existing `posts`:
|
||||
|
||||
```typescript
|
||||
save(data: Post): Observable<Post> {
|
||||
const post = { ...data, id: this.posts.length + 1, createdAt: new Date() };
|
||||
this.posts = [...this.posts, post];
|
||||
return from(this.posts);
|
||||
}
|
||||
```
|
||||
|
||||
> Here, we return the new array as result. In a real world application, most of case it is better to return the new persisted object or the id of the new post.
|
||||
|
||||
Create a test for saving a new post, verify if the length of the array is increased and if the `createdAt` is set in the new post.
|
||||
|
||||
```typescript
|
||||
it('save should increase the length of posts', done => {
|
||||
service
|
||||
.save({
|
||||
id: 4,
|
||||
title: 'test title',
|
||||
content: 'test content',
|
||||
})
|
||||
.pipe(toArray())
|
||||
.subscribe({
|
||||
next: data => {
|
||||
expect(data.length).toBe(4);
|
||||
expect(data[3].createdAt).toBeTruthy();
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Execute `npm run test` to make make sure it works.
|
||||
|
||||
Similarly, create a `update` method in `PostService` class to update the existing post.
|
||||
|
||||
```typescript
|
||||
update(id: number, data: Post): Observable<Post> {
|
||||
this.posts = this.posts.map(post => {
|
||||
if (id === post.id) {
|
||||
post.title = data.title;
|
||||
post.content = data.content;
|
||||
post.updatedAt = new Date();
|
||||
}
|
||||
return post;
|
||||
});
|
||||
return from(this.posts);
|
||||
}
|
||||
```
|
||||
|
||||
Create a test for `update` method. In the test, we updated the first item in the `posts` data store, and update it, and finally verify the changes.
|
||||
|
||||
```typescript
|
||||
it('update should change the content of post', done => {
|
||||
service
|
||||
.update(1, {
|
||||
id: 1,
|
||||
title: 'test title',
|
||||
content: 'test content',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.pipe(take(4), toArray())
|
||||
.subscribe({
|
||||
next: data => {
|
||||
expect(data.length).toBe(3);
|
||||
expect(data[0].title).toBe('test title');
|
||||
expect(data[0].content).toBe('test content');
|
||||
expect(data[0].updatedAt).not.toBeNull();
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Create a method to delete post by id.
|
||||
|
||||
```typescript
|
||||
deleteById(id: number): Observable<boolean> {
|
||||
const idx: number = this.posts.findIndex(post => post.id === id);
|
||||
if (idx >= 0) {
|
||||
this.posts = [
|
||||
...this.posts.slice(0, idx),
|
||||
...this.posts.slice(idx + 1),
|
||||
];
|
||||
return of(true);
|
||||
}
|
||||
return of(false);
|
||||
}
|
||||
```
|
||||
|
||||
Create a test case in `post.service.test` to verify the functionality of the `deleteById` method.
|
||||
|
||||
```typescript
|
||||
it('deleteById with existing id should return true', done => {
|
||||
service.deleteById(1).subscribe({
|
||||
next: data => expect(data).toBeTruthy,
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteById with none existing id should return false', done => {
|
||||
service.deleteById(10001).subscribe({
|
||||
next: data => expect(data).toBeFalsy,
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
OK, all methods used for CRUD functionalities are ready, let's move to the controller.
|
||||
|
||||
## Creating PostController
|
||||
|
||||
Like Spring WebMVC, in the NestJS world, the controller is responsible for handling incoming requests from the client, and sending back the handled result to the client.
|
||||
|
||||
Generate a controller using `nest` command:
|
||||
|
||||
```bash
|
||||
nest g co post
|
||||
```
|
||||
|
||||
It will add two files into the *src/app/post* folder.
|
||||
|
||||
```bash
|
||||
post.controller.spec.ts
|
||||
post.controller.ts
|
||||
```
|
||||
And `PostController` is registered in `PostModule` automatcially when it is generated.
|
||||
|
||||
```typescript
|
||||
//...other imports
|
||||
import { PostController } from './post.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [PostController],
|
||||
providers: [PostService]
|
||||
})
|
||||
export class PostModule {}
|
||||
```
|
||||
|
||||
Open the *post.controller.ts* file and enrich the `PostController` class as we planned.
|
||||
|
||||
```typescript
|
||||
@Controller('posts')
|
||||
export class PostController {
|
||||
constructor(private postService: PostService) {}
|
||||
|
||||
@Get('')
|
||||
getAllPosts(@Query('q') keyword?: string): Observable<BlogPost[]> {
|
||||
return this.postService.findAll(keyword).pipe(take(10), toArray());
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getPostById(@Param('id', ParseIntPipe) id: number): Observable<BlogPost> {
|
||||
return this.postService.findById(id);
|
||||
}
|
||||
|
||||
@Post('')
|
||||
createPost(@Body() post: BlogPost):Observable<BlogPost[]> {
|
||||
return this.postService.save(post).pipe(toArray());
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
updatePost(@Param('id', ParseIntPipe) id: number, @Body() post: BlogPost): Observable<BlogPost[]> {
|
||||
return this.postService.update(id, post).pipe(toArray());
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deletePostById(@Param('id', ParseIntPipe) id: number): Observable<boolean> {
|
||||
return this.postService.deleteById(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the above codes:
|
||||
|
||||
* In the `constructor`, we add `PostService` as its arguments, the `PostService` component will be injected automatically.
|
||||
* The `Controller` annotation indicates it is a controller, and it uses `/posts` as base uri all methods.
|
||||
* The `Get`, `Post`, `Put`, and `Delete` on methods are used for handling different HTTP methods.
|
||||
* The `@Param` binds the path parameters (`:id`) to the method arguments, and there is `ParseIntPipe` applied in the second arguments of Param. Pipe is a Angular concept, here it is more close to Spring's converters and validators.
|
||||
* `@Body` parses the request body into a `Post` object.
|
||||
|
||||
> In Typescript language, we call `@Controller` syntax as Decorator. For those who are familiar with Spring WebMVC, the word annotation is easier to understand.
|
||||
|
||||
Now we add tests for `PostController`.
|
||||
|
||||
Open *post.controller.spec.ts* , add the following tests.
|
||||
|
||||
```typescript
|
||||
describe('Post Controller', () => {
|
||||
let controller: PostController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PostService],
|
||||
controllers: [PostController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PostController>(PostController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('GET on /posts should return all posts', async () => {
|
||||
const posts = await controller.getAllPosts().toPromise();
|
||||
expect(posts.length).toBe(3);
|
||||
});
|
||||
|
||||
it('GET on /posts/1 should return one post ', done => {
|
||||
controller.getPostById(1).subscribe(data => {
|
||||
expect(data.id).toEqual(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('POST on /posts should return all posts', async () => {
|
||||
const post:Post = {id:4, title:'test title', content:'test content'};
|
||||
const posts = await controller.createPost(post).toPromise();
|
||||
expect(posts.length).toBe(4);
|
||||
});
|
||||
|
||||
it('PUT on /posts/1 should return all posts', done => {
|
||||
const post:Post = {id:4, title:'test title', content:'test content'};
|
||||
controller.updatePost(1, post).subscribe(data => {
|
||||
expect(data.length).toBe(3);
|
||||
expect(data[0].title).toEqual('test title');
|
||||
expect(data[0].content).toEqual('test content');
|
||||
expect(data[0].updatedAt).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE on /posts/1 should return true', done => {
|
||||
controller.deletePostById(1).subscribe(data => {
|
||||
expect(data).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE on /posts/1001 should return false', done => {
|
||||
controller.deletePostById(1001).subscribe(data => {
|
||||
expect(data).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run the tests again and make sure it works.
|
||||
|
||||
## Run the application
|
||||
|
||||
Now let's try to run the application.
|
||||
|
||||
Open a terminal, and go to the root folder of the project, and execute the following command.
|
||||
|
||||
```bash
|
||||
>npm run start
|
||||
> nestjs-sample@0.0.1 start D:\hantsylabs\nestjs-sample
|
||||
> nest start
|
||||
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [NestFactory] Starting Nest application...
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [InstanceLoader] AppModule dependencies initialized +16ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [InstanceLoader] PostModule dependencies initialized +1ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RoutesResolver] AppController {}: +10ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {, GET} route +4ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RoutesResolver] PostController {/posts}: +1ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {/posts, GET} route +1ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {/posts/:id, GET} route +2ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {/posts, POST} route +1ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {/posts/:id, PUT} route +2ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [RouterExplorer] Mapped {/posts/:id, DELETE} route +2ms
|
||||
[Nest] 9956 - 06/13/2020, 12:04:50 PM [NestApplication] Nest application successfully started +7ms
|
||||
```
|
||||
|
||||
When it is started up, it serves at http://localhost:3000.
|
||||
|
||||
Let's test the exposed APIs via `curl`:
|
||||
|
||||
```bash
|
||||
>curl http://localhost:3000/posts
|
||||
[{"id":1,"title":"Generate a NestJS project","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","content":"content","createdAt":"2020-06-13T04:20:21.920Z"}]
|
||||
|
||||
>curl http://localhost:3000/posts/1
|
||||
{"id":1,"title":"Generate a NestJS project","content":"content","createdAt":"2020-06-13T04:20:21.920Z"}
|
||||
|
||||
>curl http://localhost:3000/posts -d "{\"title\":\"new post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X POST
|
||||
[{"id":1,"title":"Generate a NestJS project","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]
|
||||
|
||||
>curl http://localhost:3000/posts/1 -d "{\"title\":\"updated post\",\"content\":\"content of my upated post\"}" -H "Content-Type:application/json" -X PUT
|
||||
[{"id":1,"title":"updated post","content":"content of my upated post","createdAt":"2020-06-13T04:20:21.920Z","updatedAt":"2020-06-13T04:21:08.259Z"},{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]
|
||||
|
||||
>curl http://localhost:3000/posts/1 -X DELETE
|
||||
true
|
||||
|
||||
>curl http://localhost:3000/posts
|
||||
[{"id":2,"title":"Create CRUD RESTful APIs","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"id":3,"title":"Connect to MongoDB","content":"content","createdAt":"2020-06-13T04:20:21.920Z"},{"title":"new post","content":"content of my new post","id":4,"createdAt":"2020-06-13T04:20:52.526Z"}]
|
||||
```
|
||||
|
||||
In the further post, we will connect to a real database instead of the dummy data.
|
||||
|
||||
Grab [the source codes from my github](https://github.com/hantsy/nestjs-sample).
|
||||
|
||||
@ -41,20 +41,20 @@ describe('PostService', () => {
|
||||
});
|
||||
|
||||
it('getPostById with existing id should return 1 post', done => {
|
||||
service
|
||||
.findById(1)
|
||||
.pipe(take(3), toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(1),
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
service.findById(1).subscribe({
|
||||
next: data => {
|
||||
expect(data.id).toBe(1);
|
||||
expect(data.title).toEqual('Generate a NestJS project');
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done(),
|
||||
});
|
||||
});
|
||||
|
||||
it('getPostById with none existing id should return empty', done => {
|
||||
service
|
||||
.findById(10001)
|
||||
.pipe(take(3), toArray())
|
||||
.pipe(toArray())
|
||||
.subscribe({
|
||||
next: data => expect(data.length).toBe(0),
|
||||
error: error => console.log(error),
|
||||
@ -109,7 +109,7 @@ describe('PostService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteById with existing id should return true', done => {
|
||||
it('deleteById with none existing id should return false', done => {
|
||||
service.deleteById(10001).subscribe({
|
||||
next: data => expect(data).toBeFalsy,
|
||||
error: error => console.log(error),
|
||||
@ -154,14 +154,22 @@ describe('PostService(test for empty())', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('getPostById with none existing id should return empty', done => {
|
||||
it('test complete for empty():getPostById with none existing id', done => {
|
||||
let called = false;
|
||||
service.findById(10001).subscribe({
|
||||
next: data => {
|
||||
console.log('complete:' + typeof data);
|
||||
expect(data).toBeNaN();
|
||||
console.log(data);
|
||||
called = true;
|
||||
},
|
||||
error: error => {
|
||||
console.log(error);
|
||||
called = true;
|
||||
},
|
||||
complete: () => {
|
||||
console.log("calling complete");
|
||||
expect(called).toBeFalsy();
|
||||
done();
|
||||
},
|
||||
error: error => console.log(error),
|
||||
complete: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Post } from './post.interface';
|
||||
import { of, from, Observable, empty } from 'rxjs';
|
||||
import { of, from, Observable, empty, EMPTY } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class PostService {
|
||||
@ -12,13 +12,13 @@ export class PostService {
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
id: 2,
|
||||
title: 'Create CRUD RESTful APIs',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
id: 3,
|
||||
title: 'Connect to MongoDB',
|
||||
content: 'content',
|
||||
createdAt: new Date(),
|
||||
@ -34,11 +34,11 @@ export class PostService {
|
||||
}
|
||||
|
||||
findById(id: number): Observable<Post> {
|
||||
const found = this.posts.filter(post => post.id === id);
|
||||
if (found.length > 0) {
|
||||
return of(found[0]);
|
||||
const found = this.posts.find(post => post.id === id);
|
||||
if (found) {
|
||||
return of(found);
|
||||
}
|
||||
return empty();
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
save(data: Post): Observable<Post> {
|
||||
@ -62,7 +62,11 @@ export class PostService {
|
||||
deleteById(id: number): Observable<boolean> {
|
||||
const idx: number = this.posts.findIndex(post => post.id === id);
|
||||
if (idx >= 0) {
|
||||
this.posts.splice(idx);
|
||||
// this.posts.splice(idx, 1);
|
||||
this.posts = [
|
||||
...this.posts.slice(0, idx),
|
||||
...this.posts.slice(idx + 1),
|
||||
];
|
||||
return of(true);
|
||||
}
|
||||
return of(false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user