mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
docs: update docs
This commit is contained in:
parent
70ef263ea5
commit
b785eb843b
@ -9,8 +9,10 @@ A NestJS RESTful APIs sample project.
|
||||
|
||||
## Docs
|
||||
|
||||
* [Building RESTful APIs with NestJS](./docs/guide.md)
|
||||
* [Getting Started](./docs/guide.md)
|
||||
* [Connecting to MongoDB](./docs/mongo.md)
|
||||
* [Protect your APIs with JWT Token](./docs/auth.md)
|
||||
* [Dealing with model relations](./docs/model.md)
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
63
docs/auth.md
63
docs/auth.md
@ -1,13 +1,13 @@
|
||||
# Protect your API resource with JWT Token
|
||||
# Protect your APIs with JWT Token
|
||||
|
||||
In the last post, we connected to a Mongo server and use a real database to replace the dummy data storage. In this post, we will explore how to protect your APIs when exposing to a client application.
|
||||
|
||||
When we come to the security of a web application, technically it will include:
|
||||
|
||||
* **Authentication** - The application will ask you to provide your principal and then it will identify who are you.
|
||||
* **Authorization**- Based on your claims, check if you have permissions to perform some tasks.
|
||||
* **Authorization**- Based on your claims, check if you have permissions to perform some operations.
|
||||
|
||||
[Passportjs](http://www.passportjs.org/) is one of the most popular authentication framework for [expressjs](https://expressjs.com/) platform. Nestjs has great integration with passportjs with `@nestjs/passportjs`. We will follow the [Authentication](https://docs.nestjs.com/techniques/authentication) chapter of the official guide to add *local* and *jwt* strategies to your application.
|
||||
[Passportjs](http://www.passportjs.org/) is one of the most popular authentication frameworks on the [Expressjs](https://expressjs.com/) platform. Nestjs has great integration with passportjs with its `@nestjs/passportjs` module. We will follow the [Authentication](https://docs.nestjs.com/techniques/authentication) chapter of the official guide to add *local* and *jwt* strategies to the application we have done the previous posts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@ -26,7 +26,9 @@ Firstly generate a `AuthModule` and `AuthService` .
|
||||
nest g mo auth
|
||||
nest g s auth
|
||||
```
|
||||
The authentication should work with users in the application. Create a standalone `UserModule` to handle user queries.
|
||||
The authentication should work with users in the application.
|
||||
|
||||
Similarly, create a standalone `UserModule` to handle user queries.
|
||||
|
||||
```bash
|
||||
nest g mo user
|
||||
@ -58,7 +60,7 @@ export class User extends Document {
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User);
|
||||
```
|
||||
The `User` class is to wrap a document in Mongo, and `UserSchema` is to describe `User` document.
|
||||
The `User` class is to wrap a document in Mongoose, and `UserSchema` is to describe `User` document.
|
||||
|
||||
Register `UserSchema` in `UserModule`, then you can use `Model<User>` to perform some operations on `User` document.
|
||||
|
||||
@ -73,7 +75,7 @@ Register `UserSchema` in `UserModule`, then you can use `Model<User>` to perfor
|
||||
export class UserModule {}
|
||||
```
|
||||
|
||||
The *users* here is the token to identify different `Model` when injecting a `Model`.
|
||||
The *users* here is used as the *token* to identify different `Model` when injecting a `Model`. When registering a `UserSchema` in mongoose, the name attribute in the above `MongooseModule.forFeature` is also the collection name of `User ` documents.
|
||||
|
||||
Add a `findByUsername` method in `UserService`.
|
||||
|
||||
@ -88,6 +90,19 @@ export class UserService {
|
||||
}
|
||||
```
|
||||
|
||||
In the `@Module` declaration of the `UserModule`, register `UserService` in `providers`, and do not forget to add it into `exports`, thus other modules can use this service when importing `UserModule`.
|
||||
|
||||
```typescript
|
||||
//...other imports
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
providers: [UserService],
|
||||
exports:[UserService]//exposing users to other modules...
|
||||
})
|
||||
export class UserModule {}
|
||||
```
|
||||
|
||||
Create a test case to test the `findByUsername` method.
|
||||
|
||||
```typescript
|
||||
@ -138,7 +153,7 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
```
|
||||
In the `@Module` declaration of the `UserModule`, register `UserService` in `providers`, and do not forget to add it into `exports`, thus other can use this service when importing `UserModule`.
|
||||
`UserService` depends on a `Model<User>`, use a provider to mock it by jest mocking feature. Using `jest.spyOn` method, you can stub the details of a methods, and watch of the calling of this method.
|
||||
|
||||
Let's move to `AuthModule`.
|
||||
|
||||
@ -174,9 +189,9 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
}
|
||||
```
|
||||
|
||||
In the constructor, use `super` to providing the essential options of the strategy you are using, for a local strategy, it requires username and password fields.
|
||||
In the constructor, use `super` to provide the essential options of the strategy you are using. For the local strategy, it requires username and password fields.
|
||||
|
||||
And the validate method it used to validate the authentication strategy against given info, here it is the *username* and *password* provided from request.
|
||||
And the validate method is used to validate the authentication info against given info, here it is the *username* and *password* provided from request.
|
||||
|
||||
> More details about the configuration options and validation of local strategy, check [passport-local](http://www.passportjs.org/packages/passport-local/) project.
|
||||
|
||||
@ -204,7 +219,7 @@ export class AuthService {
|
||||
}
|
||||
```
|
||||
|
||||
> In the real application, we could use a crypto util to hash password.
|
||||
> In the real application, we could use a crypto util to hash and compare the input password. We will discuss it in the further post.
|
||||
|
||||
It invokes `findByUsername` in `UserService` from `UserModule`. Imports `UserModule` in the declaration of `AuthModule`.
|
||||
|
||||
@ -271,11 +286,11 @@ Let's summarize how local strategy works.
|
||||
|
||||
1. When a user hits *auth/login* with `username` and `password`, `LocalAuthGuard` will be applied.
|
||||
2. `LocalAuthGuard` will trigger `LocalStrategy` , and invokes its `validate` method, and store the result back to `request.user`.
|
||||
3. Back the controller, read user principal from `request`, generate a JWT access token and send back to client.
|
||||
3. Back the controller, read user principal from `request`, generate a JWT token and send it back to the client.
|
||||
|
||||
After logging in, the `access token` can be extracted and put into the HTTP header in the new request to access the protected resources.
|
||||
|
||||
Let's have a look at how jwt strategy works.
|
||||
Let's have a look at how JWT strategy works.
|
||||
|
||||
Firstly implement the `JwtStrategy`.
|
||||
|
||||
@ -366,7 +381,9 @@ If you want to set a default strategy, change `PassportModule` in the declaratio
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
Like the data initializer for Post, add a service to insert sample data for users.
|
||||
There are several application lifecycle hooks provided in Nestjs at runtime. In your codes you can observe these lifecycle events and perform some specific tasks for your application.
|
||||
|
||||
For example, create a data initializer for `Post` to insert sample data.
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
@ -392,9 +409,27 @@ export class UserDataInitializerService
|
||||
}
|
||||
```
|
||||
|
||||
Now run the application.
|
||||
> More info about the lifecycle hooks, check the [Lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events) chapter of the official docs.
|
||||
|
||||
## Run the application
|
||||
|
||||
Open your terminal, run the application by executing the following command.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
Login using the *username/password* pair.
|
||||
|
||||
```bash
|
||||
>curl http://localhost:3000/auth/login -d "{\"username\":\"hantsy\", \"password\":\"password\"}" -H "Content-Type:application/json"
|
||||
>{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"}
|
||||
```
|
||||
|
||||
Try to access the */profile* endpoint using this *access_token*.
|
||||
|
||||
```bash
|
||||
>curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cG4iOiJoYW50c3kiLCJzdWIiOiI1ZjJkMGU0ODZhOTZiZTEyMDBmZWZjZWMiLCJlbWFpbCI6ImhhbnRzeUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlVTRVIiXSwiaWF0IjoxNTk2Nzg4NDg5LCJleHAiOjE1OTY3OTIwODl9.4oYpKTikoTfeeaUBoEFr9d1LPcN1pYqHjWXRuZXOfek"
|
||||
{"username":"hantsy","email":"hantsy@example.com","id":"5f2d0e486a96be1200fefcec","roles":["USER"]}
|
||||
```
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Building RESTful APIs with NestJS
|
||||
|
||||
In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's respective.
|
||||
In this post, I will demonstrate how to kickstart a simple RESTful APIs with NestJS from a newbie's viewpoint.
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ As described in the [Nestjs](https://nestjs.com) website, Nestjs is *a progressi
|
||||
Nestjs combines the best programming practice and the cutting-edge techniques from the NodeJS 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 flexible, you are free to choose [Expressjs](https://expressjs.com/) or [Fastify](https://www.fastify.io) as the background engine.
|
||||
* Nestjs hides the complexities of web programming in NodeJS, it provides a common abstraction of the web request handling, 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 Redis, RabbitMQ, etc.
|
||||
|
||||
If you are new to Nestjs like me but has some experience of Angular or Spring WebMVC, bootstrap a Nestjs project is really a piece of cake.
|
||||
@ -106,7 +106,7 @@ The default structure of this project is very similar with the one generated by
|
||||
* *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 the 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.service.ts* is an `@Injectable` component, similar with the service in [Angular](https://spring.io) 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 annotatoin `@Controller()` is similar with Spring MVC's `@Controller`.
|
||||
* 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.
|
||||
@ -123,9 +123,9 @@ I will reuse the concept I've used in the former examples - the blogging posts.
|
||||
|
||||
In the next steps, we will create:
|
||||
|
||||
* A new module `post` to organize all the features.
|
||||
* A new module `PostModule` to organize all these 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 `PostService` component to serve the data for controller. In this post, we use a dummy data storage temporarily, and we will replace it with a real MongoDB in the future post.
|
||||
* A `PostController` to expose APIs which is responsible for handling incoming requests.
|
||||
|
||||
|
||||
@ -151,13 +151,13 @@ import { PostModule } from './post/post.module';
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
Then generate the `PostService`, `Post` respectively.
|
||||
Then generate a `Post` service and interface respectively.
|
||||
|
||||
```bash
|
||||
nest g s post
|
||||
nest g interface post
|
||||
```
|
||||
The `PostService` is added to `PostModule` automatcially when it is generated.
|
||||
The `PostService` is added to `PostModule` automatically when it is generated.
|
||||
|
||||
```typescript
|
||||
//...other imports
|
||||
@ -169,7 +169,7 @@ import { PostService } from './post.service';
|
||||
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`.
|
||||
After it is done there are 4 files created in the *src/app/post* folder, including a *spec* file for testing `PostService`.
|
||||
|
||||
```bash
|
||||
post.interface.ts
|
||||
@ -190,7 +190,7 @@ export interface Post {
|
||||
}
|
||||
```
|
||||
|
||||
In the above interface, add some fields as you see, here we make the `id`, `createdAt`, `updatedAt` are optional now.
|
||||
In the above interface, add some fields as you see, here we make the `id`, `createdAt`, `updatedAt` *optional* now.
|
||||
|
||||
Let's create a method in `PostService` to fetch all posts.
|
||||
|
||||
@ -224,7 +224,7 @@ export class PostService {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
In the codes, we used an array `posts` as 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.
|
||||
|
||||
@ -258,7 +258,7 @@ describe('PostService', () => {
|
||||
}
|
||||
```
|
||||
|
||||
In the `beforeEach` , it prepares a `TestingModule` to assemble resources in the test.
|
||||
Similar with Angular, in the `beforeEach` hook , it prepares a `TestingModule` to assemble the required 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.
|
||||
|
||||
@ -281,12 +281,12 @@ 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.
|
||||
|
||||
> If you want to track the changes of test codes and rerun the test cases automatically, use `npm run test:watch` instead.
|
||||
|
||||
|
||||
|
||||
Let's modify the `findAll` slightly, make it accept a keyword to query the posts.
|
||||
|
||||
```typescript
|
||||
|
||||
@ -390,7 +390,7 @@ export class PostService {
|
||||
}
|
||||
```
|
||||
|
||||
As a convention in Nestjs, you have to make `PostController` `REQUEST` scoped.
|
||||
As a convention in Nestjs, you have to make `PostController` available in the `REQUEST` scoped.
|
||||
|
||||
```typescript
|
||||
@Controller({path:'posts', scope:Scope.REQUEST})
|
||||
@ -399,6 +399,100 @@ export class PostController {...}
|
||||
|
||||
In the test codes, you have to `resolve` to replace `get` to get the instance from Nestjs test harness.
|
||||
|
||||
```typescript
|
||||
describe('Post Controller', () => {
|
||||
describe('Replace PostService in provider(useClass: PostServiceStub)', () => {
|
||||
let controller: PostController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: PostService,
|
||||
useClass: PostServiceStub,
|
||||
},
|
||||
],
|
||||
controllers: [PostController],
|
||||
}).compile();
|
||||
|
||||
controller = await module.resolve<PostController>(PostController);// use resovle here....
|
||||
});
|
||||
...
|
||||
```
|
||||
|
||||
`PostService` also should be changed to request scoped.
|
||||
|
||||
```typescript
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class PostService {...}
|
||||
```
|
||||
|
||||
In the `post.service.spec.ts` , you have to update the mocking progress.
|
||||
|
||||
```typescript
|
||||
describe('PostService', () => {
|
||||
let service: PostService;
|
||||
let model: Model<Post>;
|
||||
let commentModel: Model<Comment>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PostService,
|
||||
{
|
||||
provide: POST_MODEL,
|
||||
useValue: {
|
||||
new: jest.fn(),
|
||||
constructor: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
exec: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
deleteOne: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
findOneAndUpdate: jest.fn(),
|
||||
findOneAndDelete: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: COMMENT_MODEL,
|
||||
useValue: {
|
||||
new: jest.fn(),
|
||||
constructor: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
updateOne: jest.fn(),
|
||||
deleteOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
exec: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: REQUEST,
|
||||
useValue: {
|
||||
user: {
|
||||
id: 'dummyId',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = await module.resolve<PostService>(PostService);
|
||||
model = module.get<Model<Post>>(POST_MODEL);
|
||||
commentModel = module.get<Model<Comment>>(COMMENT_MODEL);
|
||||
});
|
||||
|
||||
//...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Run the application
|
||||
|
||||
Now we have done the clean work, run the application to make sure it works as expected.
|
||||
@ -434,6 +528,6 @@ $ curl http://localhost:3000/posts/5ef0b7fe4d067321141845fc/comments
|
||||
After cleaning up the codes, we do not need the `@nestjs/mongoose` dependency, let's remove it.
|
||||
|
||||
```bash
|
||||
npm r --save @nestjs/mongoose
|
||||
npm uninstall --save @nestjs/mongoose
|
||||
```
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
In the last post , we created a RESTful API application for simple CRUD functionalities. In this post, we will enrich it:
|
||||
|
||||
* Add Mongo support
|
||||
* Add MongoDB support
|
||||
* Change dummy data storage to use MongoDB
|
||||
* Clean the testing codes
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user