A hands-on NestJS starter project exploring GraphQL (Apollo), modular architecture, and RBAC (Role-Based Access Control) patterns.
- Overview
- Tech Stack
- Prerequisites
- Getting Started
- Project Structure
- NestJS Core Concepts
- GraphQL with NestJS
- Feature Modules Breakdown
- Available Scripts
- Testing
- NestJS CLI Cheat Sheet
- Key Patterns to Remember
- Common Pitfalls
- Learning Roadmap
- Useful Resources
- License
This project is a learning sandbox for NestJS. It was scaffolded with the Nest CLI and progressively extended with feature modules that model an RBAC authentication system using GraphQL (Apollo Server, code-first approach).
All service methods currently return placeholder strings — the focus is on understanding NestJS architecture, module composition, and GraphQL wiring before connecting a real database.
| Layer | Technology | Version |
|---|---|---|
| Framework | NestJS | 11.x |
| Language | TypeScript | 5.x |
| Runtime | Node.js | ≥ 24.0 |
| API | GraphQL (Apollo Server 5 + @nestjs/graphql) |
13.x |
| HTTP Platform | Express (@nestjs/platform-express) |
11.x |
| Build Tool | SWC (via @swc/core) |
1.x |
| Testing | Jest + Supertest | 29.x |
| Linting | ESLint 9 (flat config) + Prettier | 9.x |
Before you begin, make sure you have:
Node.js ≥ 24.0.0
npm ≥ 11.0.0
Verify with:
node -v
npm -vInstall the NestJS CLI globally (optional, but recommended):
npm i -g @nestjs/cli# 1. Clone the repository
git clone https://github.com/213020aumc/nestjs-basics.git
cd nestjs-basics
# 2. Install dependencies
npm install
# 3. Start the dev server (hot-reload)
npm run start:dev
# 4. Open GraphQL Playground
# Navigate to http://localhost:3000/graphql in your browser
# 5. Test the REST health-check
# GET http://localhost:3000 → "Hello World!"nestjs-basics/
├── src/
│ ├── main.ts # App entry point — bootstraps NestFactory
│ ├── app.module.ts # Root module — imports all feature modules
│ ├── app.controller.ts # Root REST controller (GET /)
│ ├── app.service.ts # Root service (returns "Hello World!")
│ ├── app.controller.spec.ts # Unit test for AppController
│ │
│ ├── auth/ # 🔐 Authentication module
│ │ ├── auth.module.ts
│ │ ├── auth.resolver.ts # GraphQL resolver (queries + mutations)
│ │ ├── auth.service.ts # Business logic (placeholder)
│ │ ├── auth.resolver.spec.ts # Resolver unit test
│ │ ├── auth.service.spec.ts # Service unit test
│ │ ├── dto/
│ │ │ ├── create-auth.input.ts # @InputType for creating auth
│ │ │ └── update-auth.input.ts # @InputType for updating auth (PartialType)
│ │ └── entities/
│ │ └── auth.entity.ts # @ObjectType — GraphQL schema type
│ │
│ ├── users/ # 👤 Users module
│ │ ├── users.module.ts
│ │ ├── users.resolver.ts
│ │ ├── users.service.ts
│ │ ├── users.resolver.spec.ts
│ │ ├── users.service.spec.ts
│ │ ├── dto/
│ │ │ ├── create-user.input.ts
│ │ │ └── update-user.input.ts
│ │ └── entities/
│ │ └── user.entity.ts
│ │
│ ├── roles/ # 🎭 Roles module
│ │ ├── roles.module.ts
│ │ ├── roles.resolver.ts
│ │ ├── roles.service.ts
│ │ ├── roles.resolver.spec.ts
│ │ ├── roles.service.spec.ts
│ │ ├── dto/
│ │ │ ├── create-role.input.ts
│ │ │ └── update-role.input.ts
│ │ └── entities/
│ │ └── role.entity.ts
│ │
│ ├── permissions/ # 🔑 Permissions module
│ │ ├── permissions.module.ts
│ │ ├── permissions.resolver.ts
│ │ ├── permissions.service.ts
│ │ ├── permissions.resolver.spec.ts
│ │ ├── permissions.service.spec.ts
│ │ ├── dto/
│ │ │ ├── create-permission.input.ts
│ │ │ └── update-permission.input.ts
│ │ └── entities/
│ │ └── permission.entity.ts
│ │
│ └── rbac/ # 🛡️ RBAC (Role-Based Access Control) module
│ ├── rbac.module.ts
│ ├── rbac.resolver.ts
│ ├── rbac.service.ts
│ ├── rbac.resolver.spec.ts
│ ├── rbac.service.spec.ts
│ ├── dto/
│ │ ├── create-rbac.input.ts
│ │ └── update-rbac.input.ts
│ └── entities/
│ └── rbac.entity.ts
│
├── test/
│ ├── app.e2e-spec.ts # End-to-end test
│ └── jest-e2e.json # Jest E2E config
│
├── nest-cli.json # Nest CLI configuration
├── tsconfig.json # TypeScript configuration
├── tsconfig.build.json # TS config for production builds
├── eslint.config.mjs # ESLint flat config
├── .prettierrc # Prettier configuration
├── package.json # Dependencies & scripts
└── .gitignore
Modules are the fundamental organizational unit in NestJS. Every app has at least one module (the root module).
// A module groups related functionality together
@Module({
imports: [
AuthModule,
RbacModule,
UsersModule,
RolesModule,
PermissionsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Key takeaways:
imports— other modules this module depends oncontrollers— REST controllers registered in this moduleproviders— services, resolvers, and other injectablesexports— providers that should be available to other modules that import this one
Controllers handle incoming REST requests and return responses. They use decorators like
@Get(),@Post(), etc.
@Controller() // Route prefix (empty = root "/")
export class AppController {
constructor(private readonly appService: AppService) {}
@Get() // Handles GET /
getHello(): string {
return this.appService.getHello();
}
}Note: In this project, GraphQL resolvers are used instead of controllers for most features. Controllers are only used for the root health-check endpoint.
Services contain business logic and are decorated with
@Injectable(). They are injected into controllers or resolvers via the constructor.
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}Key takeaway: Keep controllers/resolvers thin — delegate logic to services.
NestJS uses a powerful DI (Dependency Injection) system. When you declare a dependency in the constructor, Nest automatically resolves and injects it:
// NestJS sees the type hint and injects the registered UsersService instance
constructor(private readonly usersService: UsersService) {}How it works:
- You register
UsersServiceas a provider inUsersModule - Any class within that module can request it via constructor injection
- Nest creates a singleton instance (by default) and shares it
NestJS relies heavily on TypeScript decorators. Here's a quick reference:
| Decorator | Purpose | Used On |
|---|---|---|
@Module() |
Declares a module | Class |
@Controller() |
Declares a REST controller | Class |
@Injectable() |
Marks a class as injectable (DI) | Class |
@Get(), @Post() |
Maps HTTP methods to handler methods | Method |
@Resolver() |
Declares a GraphQL resolver | Class |
@Query() |
Maps a method to a GraphQL query | Method |
@Mutation() |
Maps a method to a GraphQL mutation | Method |
@Args() |
Extracts arguments from a GraphQL operation | Parameter |
@ObjectType() |
Defines a GraphQL object type | Class |
@InputType() |
Defines a GraphQL input type | Class |
@Field() |
Defines a field on a GraphQL type | Property |
This project uses the code-first approach with Apollo Server.
Instead of writing .graphql schema files manually, you define your schema using TypeScript classes and decorators. NestJS auto-generates the SDL (Schema Definition Language) at runtime.
TypeScript Classes + Decorators → Auto-generated GraphQL Schema → Apollo Server
Entities define the shape of data returned by your API (equivalent to GraphQL type):
@ObjectType() // This class becomes a GraphQL type
export class User {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
}DTOs (Data Transfer Objects) define the shape of data sent TO your API:
// CreateUserInput → used for mutations that create a user
@InputType()
export class CreateUserInput {
@Field(() => Int, { description: 'Example field (placeholder)' })
exampleField: number;
}
// UpdateUserInput → extends CreateUserInput with PartialType (all fields optional) + id
@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
@Field(() => Int)
id: number;
}PartialType() pattern: Makes all inherited fields optional — perfect for update operations where you only send changed fields.
Resolvers are the GraphQL equivalent of controllers. They handle queries and mutations:
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
// QUERY: Fetch all users
@Query(() => [User], { name: 'users' })
findAll() {
return this.usersService.findAll();
}
// QUERY: Fetch single user by ID
@Query(() => User, { name: 'user' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.usersService.findOne(id);
}
// MUTATION: Create a new user
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return this.usersService.create(createUserInput);
}
// MUTATION: Update an existing user
@Mutation(() => User)
updateUser(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
return this.usersService.update(updateUserInput.id, updateUserInput);
}
// MUTATION: Delete a user
@Mutation(() => User)
removeUser(@Args('id', { type: () => Int }) id: number) {
return this.usersService.remove(id);
}
}Each feature module follows the same consistent pattern:
feature/
├── feature.module.ts # Module definition
├── feature.resolver.ts # GraphQL resolver (queries + mutations)
├── feature.service.ts # Business logic
├── feature.resolver.spec.ts # Resolver tests
├── feature.service.spec.ts # Service tests
├── dto/
│ ├── create-feature.input.ts # Input type for create mutation
│ └── update-feature.input.ts # Input type for update mutation
└── entities/
└── feature.entity.ts # GraphQL object type
Manages user data. CRUD operations via GraphQL.
| Operation | Type | GraphQL Field |
|---|---|---|
| Create user | Mutation | createUser |
| Get all users | Query | users |
| Get user by ID | Query | user |
| Update user | Mutation | updateUser |
| Delete user | Mutation | removeUser |
Handles authentication logic (login, signup, token management).
| Operation | Type | GraphQL Field |
|---|---|---|
| Create auth | Mutation | createAuth |
| Get all auth | Query | auth |
| Get auth by ID | Query | auth |
| Update auth | Mutation | updateAuth |
| Delete auth | Mutation | removeAuth |
Manages user roles (e.g., Admin, User, Moderator).
| Operation | Type | GraphQL Field |
|---|---|---|
| Create role | Mutation | createRole |
| Get all roles | Query | roles |
| Get role by ID | Query | role |
| Update role | Mutation | updateRole |
| Delete role | Mutation | removeRole |
Manages granular permissions (e.g., read:users, write:posts).
| Operation | Type | GraphQL Field |
|---|---|---|
| Create permission | Mutation | createPermission |
| Get all permissions | Query | permissions |
| Get permission by ID | Query | permission |
| Update permission | Mutation | updatePermission |
| Delete permission | Mutation | removePermission |
Orchestrates role-based access control — links users, roles, and permissions.
| Operation | Type | GraphQL Field |
|---|---|---|
| Create RBAC | Mutation | createRbac |
| Get all RBAC | Query | rbac |
| Get RBAC by ID | Query | rbac |
| Update RBAC | Mutation | updateRbac |
| Delete RBAC | Mutation | removeRbac |
| Script | Command | Description |
|---|---|---|
| Dev server | npm run start:dev |
Start with hot-reload (watch mode) |
| Start | npm run start |
Start without watch mode |
| Debug | npm run start:debug |
Start with debug + watch mode |
| Production | npm run start:prod |
Run compiled JS from dist/ |
| Build | npm run build |
Compile TypeScript to JavaScript |
| Lint | npm run lint |
Run ESLint with auto-fix |
| Format | npm run format |
Run Prettier on all TS files |
| Unit tests | npm run test |
Run all unit tests |
| Watch tests | npm run test:watch |
Run tests in watch mode |
| Coverage | npm run test:cov |
Run tests with coverage report |
| Debug tests | npm run test:debug |
Run tests with Node inspector |
| E2E tests | npm run test:e2e |
Run end-to-end tests |
| Prisma generate | npm run prisma:generate |
Generate Prisma client (future use) |
| Prisma migrate | npm run prisma:migrate |
Run Prisma migrations (future use) |
| Prisma seed | npm run prisma:seed |
Seed the database (future use) |
# Run all unit tests
npm run test
# Run tests in watch mode (re-runs on file change)
npm run test:watch
# Run tests with coverage report
npm run test:covnpm run test:e2e- Unit tests live alongside their source files:
*.spec.ts - E2E tests live in the
test/directory:*.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let app: TestingModule;
beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
});
describe('getHello', () => {
it('should return "Hello World!"', () => {
const appController = app.get(AppController);
expect(appController.getHello()).toBe('Hello World!');
});
});
});# Generate a new module
nest g module <name>
# Generate a new service
nest g service <name>
# Generate a new controller (REST)
nest g controller <name>
# Generate a new GraphQL resolver
nest g resolver <name>
# Generate a complete CRUD resource (prompts for transport layer)
nest g resource <name>
# → Choose "GraphQL (code first)" when prompted
# Generate with no test file
nest g service <name> --no-spec
# See all available generators
nest g --help
# Build the project
nest build
# Start the project
nest start --watch# This creates the full module structure in one command:
nest g resource products
# Select: GraphQL (code first)
# Select: Yes (generate CRUD entry points)
#
# Creates:
# src/products/products.module.ts
# src/products/products.resolver.ts
# src/products/products.service.ts
# src/products/dto/create-product.input.ts
# src/products/dto/update-product.input.ts
# src/products/entities/product.entity.ts
# + spec files
#
# Also auto-updates app.module.ts to import ProductsModule!Feature Module → Register in AppModule imports → Available app-wide
Entity (@ObjectType) → DTOs (@InputType) → Service (@Injectable) → Resolver (@Resolver) → Module (@Module)
// Instead of duplicating fields, extend the create input with PartialType
@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
@Field(() => Int)
id: number; // Only id is required; all other fields become optional
}// DON'T manually instantiate services
const service = new UsersService(); // ❌ Wrong
// DO let NestJS inject them
constructor(private readonly usersService: UsersService) {} // ✅ CorrectBy default, NestJS providers are singletons — the same instance is shared across the entire module. This is great for caching, connection pools, and shared state.
| Pitfall | Solution |
|---|---|
Forgot to add module to imports in AppModule |
Every feature module must be imported in the root module |
| Circular dependency between modules | Use forwardRef(() => ModuleName) |
Cannot determine a GraphQL type error |
Ensure every @Field() has an explicit type function: @Field(() => String) |
| Service not found / not injectable | Check that it's listed in providers of its module |
Duplicate imports (e.g., importing UsersModule twice) |
Clean up app.module.ts — each module should be imported only once |
| GraphQL Playground not loading | Ensure autoSchemaFile is set in GraphQLModule.forRoot() config |
Track your progress as you extend this project:
- Scaffold — Set up NestJS with TypeScript
- Modules — Create feature modules (Auth, Users, Roles, Permissions, RBAC)
- GraphQL — Integrate Apollo Server with code-first approach
- Resolvers — Wire up CRUD resolvers for each module
- DTOs — Define Input Types with PartialType pattern
- Testing — Unit tests and E2E test setup
- Database — Connect Prisma ORM (scripts already configured)
- Validation — Add
class-validatorandclass-transformerpipes - Auth Logic — Implement JWT authentication with Passport.js
- Guards — Create authentication and authorization guards
- RBAC Logic — Wire roles + permissions to guard endpoints
- Error Handling — Custom exception filters
- Logging — Structured logging with NestJS Logger
- Pagination — Implement cursor/offset pagination for queries
- Caching — Add caching layer
- Deployment — Dockerize and deploy
This project is MIT licensed.