Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/leosimoes/rocketseat-nlw-nodejs-polls
Rocketseat NLW event project using NodeJS to create Rest API and poll websockets.
https://github.com/leosimoes/rocketseat-nlw-nodejs-polls
docker fastify nodejs npm postgresql redis rest-api typescript websockets
Last synced: 24 days ago
JSON representation
Rocketseat NLW event project using NodeJS to create Rest API and poll websockets.
- Host: GitHub
- URL: https://github.com/leosimoes/rocketseat-nlw-nodejs-polls
- Owner: leosimoes
- Created: 2024-02-14T23:16:25.000Z (11 months ago)
- Default Branch: master
- Last Pushed: 2024-02-19T16:24:05.000Z (10 months ago)
- Last Synced: 2024-02-19T20:11:05.084Z (10 months ago)
- Topics: docker, fastify, nodejs, npm, postgresql, redis, rest-api, typescript, websockets
- Language: TypeScript
- Homepage:
- Size: 535 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Rocketseat - NLW - NodeJS - Polls
Rocketseat NLW event project using NodeJS to create Rest API and poll websockets.## Steps
The steps to develop the project are:
1. Create the NodeJS project in WebStorm:![Image-01-WebStorm-NodeJS](imgs/Image-01-WebStorm-NodeJS.jpg)
2. Install and configure **Typescript** through the terminal with:
- `npm install typescript @types/node -D`;
- `npx tsc --init`;
- `npm install tsx -D`.3. Configure Server:
- Create file `src/http/server.ts`;
- In `package.json`, add `"scripts": {"dev": "tsx watch src/http/server.ts"}`;
- Test execution through the terminal with `npm run dev`.4. Install and use the fastify framework:
- In the terminal, type `npm i fastify`;
- Change the `src/http/server.ts` file:```typescript
import fastify from 'fastify'const app = fastify();
app.get('/hello', () => {
return 'Hello NLW';
});app.listen({port: 3333}).then(()=>{
console.log('HTTP server running!');
});
```- Test the `http://localhost:3333/hello` route in the browser after starting the server with `npm run dev`.
![Image-02-Test-HelloRoute](imgs/Image-02-Test-HelloRoute.jpg)
5. Install and configure Docker:
- Download and install Docker Desktop according to the Operating System: `https://docs.docker.com/get-docker/`;
- Activate virtualization in the Computer BIOS: `Advanced -> CPU Configuration -> SVM Mode = Enabled`;
- Run Docker Desktop (and WebStorm) as Administrator;
- In the terminal, to check the installed version of Docker, type `docker -v`;
- In the terminal, to check the running Docker containers, type `docker ps`;
- Create the file `docker-compose.yml` whose content indicates which services the application needs;```yaml
version: '3.7'services:
postgres:
image: bitnami/postgresql:latest
ports:
- '5432:5432'
environment:
- POSTGRES_USER=docker
- POSTGRESQL_PASSWORD=docker
- POSTGRESQL_DATABASE=polls
volumes:
- polls_pg_data:/bitnami/postgresqlredis:
image: bitnami/redis:latest
ports:
- '6379:6379'
environment:
- ALLOW_EMPTY_PASSWORD=yes
volumes:
- polls_redis_data:/bitnami/redis/datavolumes:
polls_pg_data:
polls_redis_data:
```- In the terminal, to run the container services in the background, type `docker compose up -d`;
- In the terminal, to display the logs of one of the containers, type `docker logs `.6. Install Prisma and configure Prisma ORM:
- In the terminal, type `npm i -d prisma` to install Prisma;
- In the terminal, type `npx prism init`;
- change the generated file `.env`:
- from `DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"`;
- for `DATABASE_URL="postgresql://docker:docker@localhost:5432/polls?schema=public"`
- change the generated file `prisma/schema.prisma`:```prisma
generator client {
provider = "prisma-client-js"
}datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}model Poll {
id String @id @default(uuid())
title String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
}
```- In the terminal, type `npx prisma migrate dev` and then `create polls` to create migration
- In the terminal, type `npx prisma studio` to open the browser interface at `http://localhost:5555/`.7. Create `POST /polls` route:
- Install zod for validation: `npm install zod`;
- Change the file `src/http/server.ts`:```typescript
import fastify from 'fastify'
import {z} from 'zod'
import {PrismaClient} from '@prisma/client'const app = fastify();
const prism = new PrismaClient();app.post('/polls', async (request, reply)=>{
const createPollBody = z.object({
title: z.string()
});
const {title} = createPollBody.parse(request.body);
const poll = await prisma.poll.create({
date: {
title
}
});
return reply.status(201).send({pollId: poll.id})
});app.listen({port: 3333}).then(()=>{
console.log('HTTP server running!');
});
```- Test endpoint `POST http://localhost:3333/polls` with Postman:
```json
{
"title": "test1"
}
```You must separate the creation of the database connection from the route definition,
and each route must be in a file and must export an async function.8. Refactor the code and create `PollOption`:
- Change `prisma/schema.prisma` by adding `PollOption` and changing `Poll`:```prisma
model Poll {
options PollOption[]
}model PollOption {
id String @id @default(uuid())
title String
pollId Stringpoll Poll @relation(fields: [pollId], references: [id])
}
```- In the terminal, type `npx prisma migrate dev` and then `create polls options` to create migration;
- Create file `src/lib/prisma.ts`:```typescript
import {PrismaClient} from '@prisma/client'export const prism = new PrismaClient({
log: ['query']
});
```- Create file `src/http/routes/create-poll.ts`:
```typescript
import { z } from "zod";
import { prism } from '../../lib/prisma'
import { FastifyInstance} from "fastify";export async function createPoll(app: FastifyInstance){
app.post('/polls', async (request, reply)=>{
const createPollBody = z.object({
title: z.string(),
options: z.array(z.string())
});const {title, options} = createPollBody.parse(request.body);
const poll = await prisma.poll.create({
date: {
title,
options: {
createMany: {
data: options.map(option => {
return {title: option}
})
}
}
}
});return reply.status(201).send({pollId: poll.id})
})
}
```- Change `src/http/server.ts` to use the route defined in the other file:
```typescript
import fastify from 'fastify'
import {createPoll} from "./routes/create-poll";const app = fastify();
app.register(createPoll);
app.listen({port: 3333}).then(()=>{
console.log('HTTP server running!');
});
```9. Create `GET /polls` route:
- Create file `src/http/routes/get-poll.ts`:```typescript
import { z } from "zod";
import { prism } from '../../lib/prisma'
import { FastifyInstance} from "fastify";export async function getPoll(app: FastifyInstance){
app.get('/polls/:pollId', async(request, reply) =>{
const getPollParams = z.object({
pollId: z.string().uuid()
})
const { pollId } = getPollParams.parse(request.params);
const poll = await prisma.poll.findUnique({
Onde: {
id: pollId
},
include: {
options: {
select: {
id: true,
title: true
}
}
}
});return reply.send({ poll });
})
}
```- Change the file `src/http/server.ts`:
```typescript
import { getPoll } from "./routes/get-poll";
app.register(getPoll);
```10. Create route `POST /polls/:pollId/votes`:
- In the terminal, type `npm i @fastify/cookie` to be able to use cookies;
- Create file `src/http/routes/vote-on-poll.ts`:```typescript
import { z } from "zod"
import { randomUUID} from "crypto";
import { prism } from '../../lib/prisma'
import { FastifyInstance } from "fastify"export async function voteOnPoll(app: FastifyInstance) {
app.post('/polls/:pollId/votes', async(request, reply) =>{
const voteOnPollBody = z.object({
pollOptionId: z.string().uuid()
})const voteOnPollParams = z.object({
pollId: z.string().uuid()
})const { pollId } = voteOnPollParams.parse(request.params)
const { pollOptionId } = voteOnPollBody.parse(request.body)let { sessionId } = request.cookies
if(!sessionId){
sessionId = randomUUID()
reply.setCookie('sessionId', sessionId, {
path: '/',
maxAge: 60*60*24*30,
signed: true,
httpOnly: true
})
} else {
const userPreviousVoteOnPoll = await prisma.vote.findUnique({
Onde: {
sessionId_pollId: {
sessionId,
pollId
}
}
});if(userPreviousVoteOnPoll && userPreviousVoteOnPoll.pollOptionId == pollOptionId){
return reply.status(400).send({
message: "You already voted on this poll."
})
} else if (userPreviousVoteOnPoll){
await prisma.vote.delete({
Onde: {
id: userPreviousVoteOnPoll.id
}
})
}
}await prisma.vote.create({
date: {
sessionId,
pollId,
pollOptionId
}
})return reply.status(201).send()
})
}
```- Change the file `src/http/server.ts`:
```typescript
import cookie from '@fastify/cookie'
import { voteOnPoll} from "./routes/vote-on-poll";app.register(cookie, {
secret: 'my-cookie-secret-app-nlw',
hook: 'onRequest'
});
app.register(voteOnPoll);
```
- Change the generated file `prisma/schema.prisma`:```prism
model Poll {
votes Vote[]
}model PollOption {
votes Vote[]
}model Vote {
id Int @id @default(autoincrement())
sessionId String
pollId String
pollOptionId String
createAt DateTime @default(now())pollOption PollOption @relation(fields: [pollOptionId], references: [id])
poll Poll @relation(fields: [pollId], references: [id])@@unique([sessionId, pollId])
}
```- In the terminal, type `npx prisma migrate dev` and then `create votes` to create migration;
- Test with Postman and Prisma Studio:* `POST /polls`:
![Image-03-Postman-CreatePoll](imgs/Image-03-Postman-CreatePoll.jpg)
![Image-05-PrismaStudio-PollOption](imgs/Image-05-PrismaStudio-PollOption.jpg)
* `GET '/polls/:pollId`:
![Image-06-Postman-GetPoll](imgs/Image-06-Postman-GetPoll.jpg)
![Image-04-PrismaStudio-Poll](imgs/Image-04-PrismaStudio-Poll.jpg)
* `POST /polls/:pollId/votes`:
![Image-07-Postman-VotePoll-1](imgs/Image-07-Postman-VotePoll-1.jpg)
![Image-08-Postman-VotePoll-2](imgs/Image-08-Postman-VotePoll-2.jpg)
![Image-09-PrismaStudio-Vote](imgs/Image-09-PrismaStudio-Vote.jpg)
11. Use Redis to cache votes:
- In the terminal, type `npm i ioredis` to install ioredis;
- Create file `src/lib/redis.ts`:```typescript
import { Redis } from "ioredis"export const redis = new Redis()
```- Change the file `src/http/routes/vote-on-poll.ts`:
```typescript
import { redis } from "../../lib/redis";
// ...
if (userPreviousVoteOnPoll){
await redis.zincrby(pollId, -1, userPreviousVoteOnPoll.pollOptionId)
}
// ...
await redis.zincrby(pollId, 1, pollOptionId)
```- Change the file `src/http/routes/get-poll.ts`:
```typescript
import {redis} from "../../lib/redis";
// ...
if(!poll) {
return reply.status(400).send({
message: 'Poll not found.'
})
}const result = await redis.zrange(pollId, 0, -1, 'WITHSCORES')
if(result){
const votes: { [key: string]: string } = {};for (let i = 0; i < result.length; i += 2) {
const key = result[i] as string;
const value = result[i + 1] as string;
votes[key] = value;
}console.log(votes)
return reply.send({
poll: {
id: poll.id,
title: poll.title,
options: poll.options.map(option =>{
return {
id: option.id,
title: option.title,
score: (option.id in votes) ? votes[option.id] : 0
}
})
}
});
}return reply.status(500).send({
message: 'Other Errors.'
})
```12. Communicate the application backend with the frontend in Real Time using the Web Sockets protocol:
- In the terminal, type `npm i @fastify/websocket` to install fastify websockets;- Create the file `src/http/utils/voting-pub-sub.ts`:
```typescript
type Message = { pollOptionId: string, votes: number }
type Subscriber = (message: Message) => voidclass VotingPubSub {
private channels: Record = {}subscribe(pollId: string, subscriber: Subscriber){
if(!this.channels[pollId]) {
this.channels[pollId] = []
}
this.channels[pollId].push(subscriber)
}publish(pollId: string, message: Message){
if(!this.channels[pollId]){
return;
}
for(const subscriber of this.channels[pollId]){
subscriber(message)
}
}
}export const voting = new VotingPubSub();
```
- Create the file `src/http/ws/poll-results.ts`:```typescript
import { FastifyInstance } from "fastify";
import { voting } from "../../utils/voting-pub-sub";
import { z } from "zod"export async function pollResults(app: FastifyInstance){
app.get('/polls/:pollId/results', {websocket: true}, (connection, request) => {console.log("AAAAAAAAAA")
const getPollParams = z.object({
pollId: z.string().uuid()
})
const { pollId } = getPollParams.parse(request.params)voting.subscribe(pollId, (message) => {
connection.socket.send(JSON.stringify(message))
})
})
}
```- Change the file `src/http/routes/vote-on-poll.ts`:
```typescript
import {voting} from '../../utils/voting-pub-sub';
//...
else if (userPreviousVoteOnPoll){
//...
const votes = await redis.zincrby(pollId, -1, userPreviousVoteOnPoll.pollOptionId)
voting.publish(pollId, {
pollOptionId: userPreviousVoteOnPoll.pollOptionId,
votes: Number(votes)
})
}
//...
const votes = await redis.zincrby(pollId, 1, pollOptionId)voting.publish(pollId, {
pollOptionId: pollOptionId,
votes: Number(votes)
})
//...
```- Change the file `src/http/server.ts`:
```typescript
import fastifyWebsocket from "@fastify/websocket";
import {pollResults} from "./ws/poll-results";
app.register(fastifyWebsocket);
app.register(pollResults);
```- Test `ws://localhost:3333/polls/:pollId/results` with Postman:
![Image-10-Postman-PollResults](imgs/Image-10-Postman-PollResults.jpg)
## References
Docker Hub - bitnami - Postgresql:
https://hub.docker.com/r/bitnami/postgresqlDocker Hub - bitnami - Redis:
https://hub.docker.com/r/bitnami/redisPrisma:
https://www.prisma.io/ormRedis - zincrby:
https://redis.io/commands/zincrby/Redis - zrange:
https://redis.io/commands/zrange/