Overview
The Dopamine API follows NestJS patterns with:
- Controllers for HTTP request handling
- Guards for authentication/authorization
- Zod schemas for request validation
Game plan
- Create a new controller and add the module to the AppModule
- Protect the endpoint with the guard
ServerAuthGuard
: To ensure only authenticated web app requests can access this endpointWorkspaceMemberGuard
: To ensure only workspace members can access this endpoint
- Use Decorators to access the data
@CurrentUser()
: Injects the authenticated user object@CurrentWorkspace()
: Injects the current workspace object
- Use Pipes to validate and transform the data
@ZodValidationPipe()
: Validates the request with the Zod schema@IdOr404Pipe()
: To access the id of the resource or return 404 if not found
- Create Zod schemas to validate the data
Step 1: Create the Controller and Module
Let's create a Task management system as our concrete example. Start with the basic controller structure:
// apps/api/src/task/task.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { TaskService } from './task.service';
@Controller('tasks')
export class TaskController {
constructor(private readonly taskService: TaskService) {}
// Get a specific task
@Get('/:id')
async findOne(@Param('id') id: string) {
return this.taskService.findOne(id);
}
// Create a new task
@Post('/')
async create(@Body() body: any) {
return this.taskService.create(body);
}
// Update task status
@Put('/:id/status')
async updateStatus(@Param('id') id: string, @Body() body: any) {
return this.taskService.updateStatus(id, body.status);
}
// Delete a task
@Delete('/:id')
async delete(@Param('id') id: string) {
return this.taskService.delete(id);
}
}
Now create the module and register it:
// apps/api/src/task/task.module.ts
import { Module } from '@nestjs/common';
import { TaskController } from './task.controller';
import { TaskService } from './task.service';
@Module({
controllers: [TaskController],
providers: [TaskService],
exports: [TaskService], // If other modules need to use this service
})
export class TaskModule {}
Don't forget to import your Task module in the main AppModule
:
// apps/api/src/app.module.ts
import { TaskModule } from './task/task.module';
@Module({
imports: [
// ... other imports
TaskModule,
],
})
export class AppModule {}
Step 2: Protect the Endpoint with Guards
Add authentication and authorization to secure your endpoints.
Available Guards:
ServerAuthGuard
-> Primary JWT authentication guardWorkspaceMemberGuard
-> Ensures user is a member of the workspace
// apps/api/src/task/task.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards } from '@nestjs/common';
import { ServerAuthGuard } from '../auth/guards/server-auth.guard';
import { WorkspaceMemberGuard } from '../auth/guards/workspace-member.guard';
import { TaskService } from './task.service';
@Controller('tasks')
@UseGuards(ServerAuthGuard) // Apply authentication to all endpoints
export class TaskController {
constructor(private readonly taskService: TaskService) {}
@Get('/:id')
@UseGuards(WorkspaceMemberGuard) // Apply workspace membership guard
async findOne(@Param('id') id: string) {
return this.taskService.findOne(id);
}
@Post('/')
@UseGuards(WorkspaceMemberGuard) // Apply workspace membership guard
async create(@Body() body: any) {
return this.taskService.create(body);
}
@Put('/:id/status')
@UseGuards(WorkspaceMemberGuard) // Apply workspace membership guard
async updateStatus(@Param('id') id: string, @Body() body: any) {
return this.taskService.updateStatus(id, body.status);
}
@Delete('/:id')
@UseGuards(WorkspaceMemberGuard) // Apply workspace membership guard
async delete(@Param('id') id: string) {
return this.taskService.delete(id);
}
}
Step 3: Use Decorators to Access Data
Add decorators to inject user and workspace context.
Available Decorators:
@CurrentUser()
-> Injects the authenticated user object@CurrentWorkspace()
-> Injects the current workspace object
// apps/api/src/task/task.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards } from '@nestjs/common';
import { ServerAuthGuard } from '../auth/guards/server-auth.guard';
import { WorkspaceMemberGuard } from '../auth/guards/workspace-member.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { CurrentWorkspace } from '../auth/decorators/current-workspace.decorator';
import { TaskService } from './task.service';
import type { User, Workspace } from '@repo/shared-models';
@Controller('tasks')
@UseGuards(ServerAuthGuard)
export class TaskController {
constructor(private readonly taskService: TaskService) {}
// Add @CurrentUser() and @CurrentWorkspace() decorators
@Get('/:id')
@UseGuards(WorkspaceMemberGuard)
async findOne(@Param('id') id: string, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.findOne(id, user.id, workspace.id);
}
@Post('/')
@UseGuards(WorkspaceMemberGuard)
async create(@Body() body: any, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.create(body, user.id, workspace.id);
}
@Put('/:id/status')
@UseGuards(WorkspaceMemberGuard)
async updateStatus(@Param('id') id: string, @Body() body: any, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.updateStatus(id, body.status, user.id, workspace.id);
}
@Delete('/:id')
@UseGuards(WorkspaceMemberGuard)
async delete(@Param('id') id: string, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.delete(id, user.id, workspace.id);
}
}
Step 4: Create Zod Schemas to Validate Data
Define validation schemas for our Task system in the shared-models package.
// packages/shared-models/src/task/schemas.ts
import { z } from 'zod';
// Enums
export const TaskStatus = z.enum(['TODO', 'DOING', 'DONE']);
// Create Task Schema
export const createTaskSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
description: z.string().max(1000, 'Description too long').optional(),
status: TaskStatus.default('TODO'),
});
// Update Status Schema
export const updateTaskStatusSchema = z.object({
status: TaskStatus,
});
// Types
export type CreateTaskDto = z.infer<typeof createTaskSchema>;
export type UpdateTaskStatusDto = z.infer<typeof updateTaskStatusSchema>;
export type TaskStatusType = z.infer<typeof TaskStatus>;
Step 5: Use Pipes to Validate and Transform Data
Now add the pipes to validate and transform the data with our Zod schemas.
Available Pipes:
ZodValidationPipe
-> Validates request data against Zod schemasIdOr404Pipe
-> Transforms and validates resource IDs, returns 404 if not found
// apps/api/src/task/task.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards } from '@nestjs/common';
import { ServerAuthGuard } from '../auth/guards/server-auth.guard';
import { WorkspaceMemberGuard } from '../auth/guards/workspace-member.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { CurrentWorkspace } from '../auth/decorators/current-workspace.decorator';
import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe';
import { IdOr404Pipe } from '../common/pipes/id-or-404.pipe';
import { TaskService } from './task.service';
import { createTaskSchema, updateTaskStatusSchema } from '@repo/shared-models';
import type { CreateTaskDto, UpdateTaskStatusDto, User, Workspace } from '@repo/shared-models';
@Controller('tasks')
@UseGuards(ServerAuthGuard)
export class TaskController {
constructor(private readonly taskService: TaskService) {}
// Get a specific task (with ID validation)
@Get('/:id')
@UseGuards(WorkspaceMemberGuard)
async findOne(@Param('id', IdOr404Pipe) id: string, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.findOne(id, user.id, workspace.id);
}
// Create a new task (with request validation)
@Post('/')
@UseGuards(WorkspaceMemberGuard)
async create(@Body(new ZodValidationPipe(createTaskSchema)) body: CreateTaskDto, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.create(body, user.id, workspace.id);
}
// Update task status (with ID and body validation)
@Put('/:id/status')
@UseGuards(WorkspaceMemberGuard)
async updateStatus(
@Param('id', IdOr404Pipe) id: string,
@Body(new ZodValidationPipe(updateTaskStatusSchema)) body: UpdateTaskStatusDto,
@CurrentUser() user: User,
@CurrentWorkspace() workspace: Workspace
) {
return this.taskService.updateStatus(id, body.status, user.id, workspace.id);
}
// Delete a task (with ID validation)
@Delete('/:id')
@UseGuards(WorkspaceMemberGuard)
async delete(@Param('id', IdOr404Pipe) id: string, @CurrentUser() user: User, @CurrentWorkspace() workspace: Workspace) {
return this.taskService.delete(id, user.id, workspace.id);
}
}
Conclusion
This complete Task management example demonstrates all the key patterns for building secure, well-structured endpoints in the Dopamine API, including authentication, workspace context, validation and business logic.