{"id":22451935,"url":"https://github.com/jsericksk/quotes-api","last_synced_at":"2026-04-12T05:35:33.939Z","repository":{"id":177450357,"uuid":"651544665","full_name":"jsericksk/Quotes-Api","owner":"jsericksk","description":"Quotes API using TypeScript, NodeJS and Express.","archived":false,"fork":false,"pushed_at":"2023-07-10T19:41:11.000Z","size":429,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-27T12:35:13.701Z","etag":null,"topics":["express","jest-test","nodejs","postgresql","supertest","typescript","zod-validators"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsericksk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-09T13:28:12.000Z","updated_at":"2023-07-18T09:30:41.000Z","dependencies_parsed_at":null,"dependency_job_id":"b45742be-45a0-41d5-a692-e259e8a53bd9","html_url":"https://github.com/jsericksk/Quotes-Api","commit_stats":null,"previous_names":["jsericksk/quotes-api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jsericksk/Quotes-Api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsericksk%2FQuotes-Api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsericksk%2FQuotes-Api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsericksk%2FQuotes-Api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsericksk%2FQuotes-Api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsericksk","download_url":"https://codeload.github.com/jsericksk/Quotes-Api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsericksk%2FQuotes-Api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31705574,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-12T05:11:36.334Z","status":"ssl_error","status_checked_at":"2026-04-12T05:11:27.332Z","response_time":58,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["express","jest-test","nodejs","postgresql","supertest","typescript","zod-validators"],"created_at":"2024-12-06T06:09:20.494Z","updated_at":"2026-04-12T05:35:33.934Z","avatar_url":"https://github.com/jsericksk.png","language":"TypeScript","readme":"\u003ch1 align=\"center\"\u003eQuotes API\u003c/h2\u003e\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://skillicons.dev/icons?i=ts,nodejs,postgres,jest\u0026theme=dark\" alt=\"tech-info\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"screenshots/screenshot-01.png\" width=\"850\" height=\"478\" /\u003e\n  \u003cimg src=\"screenshots/screenshot-02.png\" width=\"850\" height=\"478\" /\u003e\n\u003c/p\u003e\n\n## Objetivo\n\nEsse é um projeto básico criado com o objetivo de aprender mais sobre APIs/backend, assim como **TypeScript** e **NodeJS**. É um projeto com fins unicamente de estudos.\n\n## :pencil2: Funcionalidades\n\nA API tem como principal função publicar e obter frases, sendo necessário se registrar para tal. Possui um CRUD completo de frases para usuários logados, com opções de adicionar, obter, atualizar e excluir frases, além de contar com o recurso de paginação ao obter as frases. Os dados são armazenados em um banco de dados **PostegreSQL**.\n\n## :hammer_and_wrench: Principais tecnologias e bibliotecas utilizadas\n\n- [NodeJS](https://nodejs.org/en) e [Express](https://expressjs.com);\n- [Knex](https://github.com/knex/knex): Query builder;\n- [PostgreSQL](https://www.postgresql.org): Banco de dados;\n- [Jest](https://github.com/jestjs/jest) e [SuperTest](https://github.com/ladjs/supertest): Testes de integração;\n- [Bcrypt.js](https://github.com/dcodeIO/bcrypt.js): Criptografia de senhas;\n- [Zod](https://github.com/colinhacks/zod): Validação de requisições;\n- [Http-Status-Codes](https://github.com/prettymuchbryce/http-status-codes): Status codes mais legíveis.\n\n## :rocket: Executar localmente\n\nEstou utilizando o **yarn**, mas você pode utilizar o gerenciador de pacotes de sua preferência. Antes de executar localmente, é necessário definir as variáveis de ambiente no arquivo **.env**. Você precisa criar o arquivo .env e copiar as variáveis do **.env.example**, preenchendo com os valores de configuração adequados.\n\n**IMPORTANTE**: *ACCESS_TOKEN_SECRET_KEY* e *REFRESH_TOKEN_SECRET_KEY* devem ter valores diferentes, caso contrário, um refresh token poderá ser usado como access token.\n\n- **yarn dev**: Executar localmente.\n- **yarn test** ou **yarn jest**: Rodar todos os testes.\n- **yarn knex:migrate:** Executar migrations.\n- **yarn knex:rollback:** Executar rollback.\n- **yarn knex:seed:** Executar seeds.\n\n## :memo: API Docs\n\nRotas **/auth**:\n- [Registrar usuário](#registrar-usuário)\n- [Login](#login)\n- [Refresh token](#refresh-token)\n\nRotas **/quotes**:\n- [Obter todas as frases](#obter-todas-as-frases)\n- [Obter frase por id](#obter-frase-por-id)\n- [Publicar frase](#publicar-frase)\n- [Atualizar frase](#atualizar-frase)\n- [Excluir frase](#excluir-frase) \n\nManipulação de erros:\n- [Error handling](#error-handling) \n\n### Registrar usuário\n\n\u003e POST /auth/register\n\n- **Request - Body:**\n\n```json\n{\n  \"email\": \"Email do usuário\",\n  \"username\": \"Nome do usuário. Deve conter no mínimo 3 caracteres e no máximo 50\",\n  \"password\": \"Senha do usuário. Deve conter no mínimo 6 caracteres\"\n}\n```\n\n- **Response - Status 201:** ID do usuário registrado (int).\n\n### Login\n\n\u003e POST /auth/login\n\n- **Request - Body:**\n\n```json\n{\n  \"email\": \"Email do usuário\",\n  \"password\": \"Senha do usuário. Deve conter no mínimo 6 caracteres\"\n}\n```\n\n- **Response - Status 200:** \n\n```json\n{\n  \"accessToken\": \"Access token válido por 1h\",\n  \"refreshToken\": \"Refresh token válido por 7 dias (ou até ser utilizado para gerar um novo access token)\"\n}\n```\n\n### Refresh token\n\n\u003e POST /auth/refresh-token\n\n- **Request - Body:**\n\n```json\n{\n  \"refreshToken\": \"Refresh token válido no formato JWT gerado durante o login\"\n}\n```\n\n- **Response - Status 200:** \n\n```json\n{\n  \"accessToken\": \"Novo access token válido por 1h\",\n  \"refreshToken\": \"Novo refresh token válido por 7 dias (ou até ser utilizado para gerar um novo access token)\"\n}\n```\n\n### Obter todas as frases\n\n**NOTA IMPORTANTE:** Todas as rotas de frases são protegidas e precisam de um Authorization Header.  \nPasse **Authorization: Bearer ***access token***** no header de requisição.\n\n- Você pode pesquisar por frases ao fornecer um **filter** e obter todas as frases de um determinado usuário ao fornecer o **userId**.\n\n\u003e GET /quotes\n\n\u003e GET /quotes?page=1\n\n\u003e GET /quotes?filter=conhecimento\n\n\u003e GET /quotes?userId=1\n\n\u003e GET /quotes?page=1\u0026filter=conhecimento\n\n\u003e GET /quotes?userId=1\u0026filter=conhecimento\n\n- **Response - Status 200:**\n\n```json\n{\n  \"info\": {\n    \"count\": 20,\n    \"pages\": 2,\n    \"next\": \"/quotes?page=2\",\n    \"previous\": null\n  },\n  \"results\": [\n    {\n      \"id\": 1,\n      \"quote\": \"A imaginação é mais importante que o conhecimento.\",\n      \"author\": \"Albert Einstein\",\n      \"postedByUsername\": \"John\",\n      \"postedByUserId\": 1,\n      \"publicationDate\": \"2023-06-21T15:20:28.936Z\"\n    },\n    [...]\n  ]\n}\n```\n\n- **count**: Número total de frases.\n- **pages**: Número total de páginas disponíveis.\n- **next**: Próxima página. null se não houver nenhuma.\n- **previous**: Página anterior. null se não houver nenhuma.\n- **results**: Frases encontradas.\n- O limite de frases por página é 15.\n\n### Obter frase por id\n\n\u003e GET /quotes/:id\n\n- **Response - Status 200:** \n\n```json\n{\n  \"id\": 1,\n  \"quote\": \"A imaginação é mais importante que o conhecimento.\",\n  \"author\": \"Albert Einstein\",\n  \"postedByUsername\": \"John\",\n  \"postedByUserId\": 1,\n  \"publicationDate\": \"2023-06-21T15:20:28.936Z\"\n}\n```\n\n### Publicar frase\n\n\u003e POST /quotes\n\n- **Request - Body:**\n\n```json\n{\n  \"quote\": \"Frase. Deve conter no mínimo 7 caracteres e no máximo 1000\",\n  \"author\": \"Nome do autor. Deve conter no mínimo 1 caractere e no máximo 80\"\n}\n```\n\n- **Response - Status 201:** \n\n```json\n{\n  \"id\": 1,\n  \"quote\": \"A imaginação é mais importante que o conhecimento.\",\n  \"author\": \"Albert Einstein\",\n  \"postedByUsername\": \"John\",\n  \"postedByUserId\": 1,\n  \"publicationDate\": \"2023-06-21T15:20:28.936Z\"\n}\n```\n\n### Atualizar frase\n\n\u003e PUT /quotes/:id\n\n- **Request - Body:** \n\n```json\n{\n  \"quote\": \"Frase atualizada. Deve conter no mínimo 7 caracteres e no máximo 1000\",\n  \"author\": \"Nome do autor atualizado. Deve conter no mínimo 1 caractere e no máximo 80\"\n}\n```\n\n- **Response - Status 204:** Não há body de resposta.\n\n### Excluir frase\n\n\u003e DELETE /quotes/:id\n\n- **Response - Status 204:** Não há body de resposta.\n\n### Error handling\n\nTodos os endpoints que precisam de um **body/params/query** podem retornar uma **BAD_REQUEST (400)** se a requisição contiver valores inválidos. Exemplos de erros:\n\n- Login:\n```json\n{\n  \"errors\": {\n    \"body\": {\n      \"email\": \"Invalid email\",\n      \"password\": \"String must contain at least 6 character(s)\"\n    }\n  }\n}\n```\n\n- Tentar obter todas as frases com **?page=abc**:\n```json\n{\n  \"errors\": {\n    \"query\": {\n      \"page\": \"Expected number, received nan\"\n    }\n  }\n}\n```\n\n- Atualizar frase com campos no body e id inválidos:\n```json\n{\n  \"errors\": {\n    \"body\": {\n      \"quote\": \"String must contain at least 7 character(s)\",\n      \"author\": \"String must contain at least 1 character(s)\"\n    },\n    \"params\": {\n      \"id\": \"Expected number, received nan\"\n    }\n  }\n}\n```\n\nTodos os endpoints podem retornar um body contendo um **error** e **error_code** padrão. **error_code** sempre será retornado com um valor \"unespecifed\" caso não seja importante distinguir o tipo de erro. Códigos de erro disponíveis:\n\n- **email_already_exists:** Quando tentar se registrar com e-mail que já existe.\n- **username_already_exists:** Quando tentar se registrar com nome de usuário que já existe.\n- **search_without_results:** Quando nenhuma frase com o \"filter\" aplicado for encontrada.\n- **user_without_posts:** Quando o usuário não possuir nenhuma frase publicada.\n- **invalid_page:** Quando uma página inválida for informada na paginação.\n\n```json\n{\n  \"error\": \"Email not available\",\n  \"error_code\": \"email_not_available\"\n}\n```\n\nAlguns endpoints podem retornar **NOT_FOUND (404)** se o recurso não for encontrado ou a frase não pertencer ao usuário.\n\n- Tentar atualizar ou excluir com o id de uma frase que não existe ou não pertence ao usuário:\n\n```json\n{\n  \"error\": \"There is no quote with the given id\",\n  \"error_code\": \"unspecified\"\n}\n```\n\n- Tentar obter frases com uma **page** maior do que a quantidade de páginas disponíveis:\n\n```json\n{\n  \"error\": \"Invalid page, no more items\",\n  \"error_code\": \"invalid_page\"\n}\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsericksk%2Fquotes-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsericksk%2Fquotes-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsericksk%2Fquotes-api/lists"}