- Features
- Project Structure
- Getting Started
- API Documentation
- Testing
- Key Concepts and Implementation
- GraphQL Integration
This project is a CRUD (Create, Read, Update, Delete) API built with NestJS, TypeORM, and PostgreSQL. It demonstrates an API including authentication, authorization, and data validation.
- User management (CRUD operations)
- Post management (CRUD operations)
- Comment management (CRUD operations)
- Authentication using JWT
- Authorization using guards
- Data validation using pipes
- API documentation using Swagger
The project follows a modular structure, with separate modules for users, posts, comments, and authentication. Each module contains its own controllers, services, and DTOs.
- Clone the repository
- Install dependencies:
npm install - Set up your PostgreSQL database. I ran it in a
podmancontainer running the postgres image hosted on dockerhub
# pull the image
podman pull postgres:13
# run the image
podman run -d \
--name postgres_db_for_crud_app \
-e POSTGRES_PASSWORD=<from datasource.ts> \
-e POSTGRES_USER=<from datasource.ts>\
-e POSTGRES_DB=<from datasource.ts>\
-p 5432:5432 \
postgres:13
# check that it's running
podman ps- Run migrations:
npm run migration:run(NOTE: uncomment them first. had issues with the nest project compilation configs properly dealing with those migration files when they're in/srcbut I wanted to keep it all in the same repo. After migrations run, comment the migration files out again before starting the nest dev server) - Start the server:
npm run start:dev
API documentation is available via Swagger UI. After starting the server, navigate to http://localhost:3000/api in your web browser to view the interactive API documentation.
Run the following command to execute end-to-end tests:
npm run test:e2e
I focused on more integration-y tests because I feel like they're more valuable. They take a little more to write but they also test more and they test things in the ways that real users interact with a system.
The ClassSerializerInterceptor in NestJS works in conjunction with class-transformer decorators to exclude or expose specific properties when serializing objects. However, it doesn't automatically exclude sensitive information on its own. The exclusion of sensitive data, like passwords, is actually achieved through the use of the @Exclude() decorator from the class-transformer library.
Here's how it works in this project:
- In the
Userentity (src/users/user.entity.ts), thepasswordfield is decorated with@Exclude():
import { Exclude } from 'class-transformer';
@Entity('user')
export class User {
// ... other fields
@Column()
@Exclude()
password: string;
// ... other fields
}- The
ClassSerializerInterceptoris applied to theUsersController:
@UseInterceptors(ClassSerializerInterceptor)
@Controller('users')
export class UsersController {
// ... controller methods
}- When the
ClassSerializerInterceptorprocesses the response, it uses theclass-transformerlibrary to serialize theUserobjects. During this serialization, any properties marked with@Exclude()(like thepasswordfield) are omitted from the output.
So, the combination of the @Exclude() decorator and the ClassSerializerInterceptor ensures that sensitive information like passwords is not included in the API responses.
It's important to note that this exclusion happens at the serialization level, which means:
- The
passwordfield still exists in the database and in theUserobjects within the application. - The exclusion only applies when the object is being serialized for a response, typically when sending data back to the client.
This approach provides a clean way to manage which data is exposed via your API without having to manually filter it in each route handler.
Guards are used for authentication and authorization. The project uses two main guards:
JwtAuthGuard: Used to protect routes that require authentication.
Example usage in UsersController:
@UseGuards(JwtAuthGuard)
@Controller('users')
export class UsersController {
// ... controller methods
}LocalStrategyguard: Used for email/password authentication during login.
Example usage in AuthController:
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}The project includes a custom @Public() decorator to mark routes that should be publicly accessible without authentication.
Definition in src/auth/public.decorator.ts:
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Usage in UsersController:
@Public()
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}Pipes in NestJS are used for data transformation and validation. This project primarily uses the ValidationPipe, which is applied globally to validate incoming request data against DTO (Data Transfer Object) schemas.
The ValidationPipe is configured globally in main.ts:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);This configuration does the following:
whitelist: true: Strips out properties that don't have any decorators in the DTO.forbidNonWhitelisted: true: Throws an error if non-whitelisted properties are present.transform: true: Automatically transforms incoming payloads to be instances of their respective DTO classes.
The ValidationPipe works in conjunction with DTO classes that use class-validator decorators. For example, in the CreateUserDto:
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsOptional()
@IsString()
bio?: string;
}When a request is made to create a user, the ValidationPipe will:
- Validate that
emailis a valid email address. - Ensure that
passwordis a string with a minimum length of 6 characters. - Allow
bioto be optional, but if present, ensure it's a string. - Remove any additional properties not defined in the DTO.
- If any validation fails, it will return a 400 Bad Request error with details about the validation failure.
Benefits of using the ValidationPipe include:
- Automatic validation of incoming data
- Type safety when working with data in controllers and services
- Enhanced security by stripping unexpected properties
- Cleaner controller code by moving validation logic to DTOs
This approach ensures that only valid data is processed by the application, enhancing overall data integrity and security.
This project could benefit from implementing query caching using NestJS's built-in cache module to improve performance and reduce database load. Here's an overview of how caching could be implemented:
-
Install required packages:
npm install @nestjs/cache-manager cache-manager -
Configure caching in
AppModule:import { CacheModule, CacheInterceptor } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; @Module({ imports: [ CacheModule.register({ ttl: 60 * 5, // 5 minutes max: 100, // maximum number of items in cache }), // other imports ], providers: [ { provide: APP_INTERCEPTOR, useClass: CacheInterceptor, }, // other providers ], }) export class AppModule {}
-
Apply caching to services or controllers:
@Injectable() @UseInterceptors(CacheInterceptor) export class UsersService { @CacheTTL(30) // Cache for 30 seconds async findAll(): Promise<User[]> { return await this.usersRepository.find(); } }
@Controller('users') @UseInterceptors(CacheInterceptor) export class UsersController { @Get() @CacheTTL(60) // Cache for 60 seconds findAll() { return this.usersService.findAll(); } }
Implementing caching would significantly reduce database load for frequently accessed data. Cache duration could be adjusted as needed for different endpoints or services. Future enhancements could include using Redis for distributed caching and implementing automatic cache invalidation strategies.
This project could be enhanced by implementing WebSockets to enable real-time, bidirectional communication between the server and clients. Here's an overview of how WebSockets could be integrated and utilized:
-
Install required package:
npm install @nestjs/websockets @nestjs/platform-socket.io -
Create a new WebSocket gateway (e.g.,
src/websockets/events.gateway.ts):import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody } from '@nestjs/websockets'; import { Server } from 'socket.io'; @WebSocketGateway() export class EventsGateway { @WebSocketServer() server: Server; @SubscribeMessage('newPost') handleNewPost(@MessageBody() data: any): void { this.server.emit('postCreated', data); } }
-
Import the WebSocket gateway in
AppModule:import { Module } from '@nestjs/common'; import { EventsGateway } from './websockets/events.gateway'; @Module({ // ...other imports providers: [EventsGateway], }) export class AppModule {}
Potential Use Cases:
-
Real-time post updates:
- When a new post is created, notify all connected clients.
- Update post list on client-side without refreshing the page.
-
Live commenting:
- Show new comments instantly across all connected clients.
-
User activity notifications:
- Notify users when someone likes their post or follows them.
-
Collaborative editing:
- Implement real-time collaborative editing for posts or comments.
Benefits:
- Enhanced user experience with instant updates.
- Reduced server load by minimizing polling requests.
- Enables building more interactive and dynamic features.
- Facilitates real-time data synchronization across multiple clients.
Implementing WebSockets would significantly improve the real-time capabilities of the application, making it more engaging and responsive to user actions.
This project incorporates GraphQL alongside the REST API, providing a flexible and efficient way to query and manipulate data. The GraphQL implementation uses NestJS's built-in support for GraphQL with the @nestjs/graphql package and the Apollo server.
The GraphQL module is configured in the AppModule (src/app.module.ts):
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
// ... other imports
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'schema.gql',
}),
// ... other modules
],
})
export class AppModule {}This configuration:
- Uses the Apollo driver for GraphQL
- Automatically generates a GraphQL schema file (
schema.gql) based on the application's GraphQL definitions
The project uses resolvers to define GraphQL operations. For example, the UsersResolver (src/users/users.resolver.ts) defines queries and mutations for user-related operations:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'
import { UsersService } from './users.service'
import { User } from './user.schema'
@Resolver(() => User)
export class UsersResolver {
constructor(private usersService: UsersService) {}
@Query(() => [User])
async users(): Promise<User[]> {
return this.usersService.findAll()
}
@Query(() => User)
async user(@Args('id') id: number): Promise<User> {
return this.usersService.findOne(id)
}
@Mutation(() => Boolean)
async removeUser(@Args('id') id: number): Promise<void> {
return this.usersService.remove(id)
}
}GraphQL object types are defined using classes with decorators from @nestjs/graphql. For instance, the User type is defined in src/users/user.schema.ts:
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(type => ID)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field()
email: string;
@Field({ nullable: true })
bio?: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}Once the server is running, you can access the GraphQL Playground at http://localhost:3000/graphql. This interactive environment allows you to explore the GraphQL schema, write and execute queries and mutations, and view the results.
- Flexible Data Fetching: Clients can request exactly the data they need, no more, no less.
- Single Endpoint: All data operations go through a single endpoint, simplifying API management.
- Strong Typing: The GraphQL schema provides a clear contract between client and server.
- Efficient: Reduces over-fetching and under-fetching of data, which can be common in REST APIs.
This project demonstrates how GraphQL can coexist with a traditional REST API. This dual approach allows for flexibility in how clients interact with the server, catering to different use cases and client requirements.
By incorporating both REST and GraphQL, the project showcases a modern, flexible API design that can adapt to various client needs and preferences.