Torres del Paine, Patagonia
Storytelling Techniques
Storytelling Techniques
Storytelling Techniques
Why am I telling this story?
What do I want people to know / have learnt?
i love the java ecosystem
... then I was put on a node project...
..., and now my reality is serverless
- Startup of JVM is super slow
- JVM uses lots of RAM, especially threaded webserver under load
- Customer is incredibly ops cost sensitive
- Focus on on demand resources like Lambdas
Blocking I/O. Each connection consumes significant memory and waits for database/disk responses.
Non-blocking. Offloads I/O tasks and continues processing other requests in the event loop.
However, requires understanding of micro and macro task queue which might run into starvation bugs if used incorrectly.
vs.
async function saveOrder(order: Order) {
const user = await db.users.find(order.uid);
const result = await db.orders.insert(order);
await email.send(user.email, "Success!");
return result;
}
// Mock DB
const db = {
users: {
find: async (id: string): Promise<User | null> => {
return { id, email: "user@example.com" };
},
},
...
};
//Service
const emailService = {
send: async (email: string, message: string):
Promise<void> => {
console.log(`Email sent to ${email}: ${message}`);
},
};
public Mono<Order> saveOrder(Order order) {
return userRepository.findById(order.getUid())
.flatMap(user -> orderRepository.save(order)
.flatMap(result -> emailService.send(user.getEmail())
.thenReturn(result)));
}
// Repository
public interface UserRepository {
Mono<User> findById(String id);
}
public interface OrderRepository {
Mono<Order> save(Order order);
}
// Service
public class EmailService {
public Mono<Void> send(String email, String message) {
return Mono.fromRunnable(() ->
System.out.println("Email sent to " + email)
);
}
}
suspend fun saveOrderParallel(order: Order): Order = coroutineScope {
val userDeferred = async { userRepository.findById(order.uid) }
val saveDeferred = async { orderRepository.save(order) }
val user = userDeferred.await()
?: throw IllegalStateException("User not found")
val savedOrder = saveDeferred.await()
emailService.send(user.email, "Order success!")
savedOrder
}
{
"strict": true,
"noImplicitAny": true,
"exactOptionalPropertyTypes": true
}
import { z } from "zod";
export const CustomerSchema = z.object({
id: z.string().uuid(),
firstName: z.string().min(1),
email: z.string().email().optional(),
loyaltyPoints: z.number().int().nonnegative(),
});
/**
* Infer TypeScript type from schema
*/
export type Customer = z.infer<typeof CustomerSchema>;
/*
type Customer = {
id: string;
firstName: string;
email?: string | undefined;
loyaltyPoints: number;
}
*/
// 1. infer return type with ReturnType<>
function createCustomer() {
return {
name: "Alice",
age: 30,
};
}
type Customer = ReturnType<typeof createCustomer>; // { name: string; age: number;}
// 2. Extracts parameter types of a function as a tuple
function updateCustomer(id: string, age: number) {
return true;
}
type UpdateParams = Parameters<typeof updateCustomer>; // [id: string, age: number]
//3. Remove properties from a type
type User = {
id: string;
email: string;
password: string;
};
type PublicCustomer = Omit<User, "password">; // { id: string, email: string;}
Most Node frameworks prioritize a small footprint and raw performance. They provide the bare essentials, letting developers bolt on only what they need.
Being unopinionated means the framework won't tell you how to structure your folders, handle DI, or manage state.
NestJS is heavily inspired by Angular and Spring. It brings enterprise-grade structure to Node.js, solving the "architecture-less" problem of Express.
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get()
findAll() { return this.userService.findAll(); }
}
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findById(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
}
Controller
@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {}
async findById(id: string) {
const user = await this.repo.findById(id);
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
create(dto: { name: string; email: string }) {
return this.repo.save(dto);
}
}
Service
import { Module } from '@nestjs/common';
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
})
export class UsersModule {}
Module
import { IsEmail, IsString } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
Model
Modern JS projects often inherit 1000+ transitive dependencies. Every single one is a potential vector for malicious code injection.