├── client ├── public │ └── .gitkeep ├── src │ ├── styles │ │ ├── base │ │ │ ├── .gitkeep │ │ │ └── _common.scss │ │ ├── pages │ │ │ ├── .gitkeep │ │ │ ├── _about.scss │ │ │ ├── _docs.scss │ │ │ └── _home-page.scss │ │ ├── themes │ │ │ └── .gitkeep │ │ ├── vendors │ │ │ └── .gitkeep │ │ ├── abstracts │ │ │ ├── .gitkeep │ │ │ ├── _variables.scss │ │ │ └── _animations.scss │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── _paypal-button.scss │ │ │ └── _banner-ad.scss │ │ ├── layout │ │ │ ├── .gitkeep │ │ │ ├── _footer.scss │ │ │ ├── _swapi-header.scss │ │ │ ├── _navbar.scss │ │ │ └── _layout.scss │ │ └── main.scss │ ├── assets │ │ └── images │ │ │ └── saber-masters │ │ │ ├── bogo-text.webp │ │ │ ├── dual-grey.webp │ │ │ ├── dual-red.webp │ │ │ ├── gold-black.webp │ │ │ ├── jedi-hand.webp │ │ │ ├── logo_new.avif │ │ │ ├── red-banner.webp │ │ │ ├── dual-silver.webp │ │ │ ├── gold-silver.webp │ │ │ ├── red-banner-2.webp │ │ │ ├── red-banner-3.webp │ │ │ ├── silver-gunsmoke.webp │ │ │ ├── Cinematic_SI_UGC_Tiara.avif │ │ │ └── silver-black-blue-blades.webp │ ├── main.jsx │ ├── components │ │ ├── SwapiHeader.jsx │ │ ├── Footer.jsx │ │ ├── Navbar.jsx │ │ ├── affiliate │ │ │ ├── BannerAds.jsx │ │ │ └── PayPalButton.jsx │ │ └── common │ │ │ ├── LoadingSpinner.jsx │ │ │ └── AtAtSpinner.jsx │ ├── hooks │ │ └── useDebounce.js │ ├── App.jsx │ ├── layouts │ │ └── Layout.jsx │ └── pages │ │ ├── AdClickRedirectPage.jsx │ │ ├── HomePage.jsx │ │ ├── AboutPage.jsx │ │ └── DocsPage.jsx ├── vite.config.js ├── package.json ├── eslint.config.js └── index.html ├── Procfile ├── ads.txt ├── server ├── index.js ├── utils │ ├── isWookiee.js │ ├── cache.js │ └── wookieeEncoding.js ├── middleware │ ├── connectDb.js │ ├── encodingFormat.js │ ├── setUrl.js │ ├── checkKey.js │ ├── limiters.js │ └── addAdURL.js ├── routes │ ├── rootRoutes.js │ ├── countRoutes.js │ ├── adClickRoutes.js │ ├── filmRoutes.js │ ├── planetRoutes.js │ ├── peopleRoutes.js │ ├── speciesRoutes.js │ ├── vehicleRoutes.js │ └── starshipRoutes.js ├── models │ ├── AdClickModel.js │ ├── PlanetModel.js │ ├── FilmModel.js │ ├── SpeciesModel.js │ ├── PeopleModel.js │ ├── VehicleModel.js │ └── StarshipModel.js ├── config │ ├── config.js │ └── dbConfig.js ├── services │ ├── rootService.js │ ├── filmService.js │ ├── countService.js │ ├── peopleService.js │ ├── planetService.js │ ├── speciesService.js │ ├── vehicleService.js │ ├── adClickService.js │ └── starshipService.js ├── controllers │ ├── countController.js │ ├── rootController.js │ ├── filmController.js │ ├── adClickController.js │ ├── peopleController.js │ ├── vehicleController.js │ ├── starshipController.js │ ├── planetController.js │ └── speciesController.js ├── package.json ├── app │ ├── routes.js │ ├── index.js │ └── middleware.js └── helpers │ └── pagination.js ├── .editorconfig ├── .gitignore ├── package.json ├── LICENSE ├── CONTRIBUTORS.md ├── CONTRIBUTING.md └── README.md /client/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-6008860561343019, DIRECT, f08c47fec0942fa0 2 | -------------------------------------------------------------------------------- /client/src/styles/base/.gitkeep: -------------------------------------------------------------------------------- 1 | Resets, typography, general global styles -------------------------------------------------------------------------------- /client/src/styles/pages/.gitkeep: -------------------------------------------------------------------------------- 1 | Page-specific styles (home, about, docs, etc) -------------------------------------------------------------------------------- /client/src/styles/themes/.gitkeep: -------------------------------------------------------------------------------- 1 | Different them styles (light, dark mode, etc) -------------------------------------------------------------------------------- /client/src/styles/vendors/.gitkeep: -------------------------------------------------------------------------------- 1 | Third-party styles (Bootstrap, external css, etc) -------------------------------------------------------------------------------- /client/src/styles/abstracts/.gitkeep: -------------------------------------------------------------------------------- 1 | Global helpers (variables, functions, mixins, etc) -------------------------------------------------------------------------------- /client/src/styles/components/.gitkeep: -------------------------------------------------------------------------------- 1 | Reusable UI copmonents (buttons, cards, navbar) -------------------------------------------------------------------------------- /client/src/styles/layout/.gitkeep: -------------------------------------------------------------------------------- 1 | Structural styles (header, footer, grid, sidebar, etc) -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const { startServer } = require("./app"); 2 | 3 | startServer(); 4 | -------------------------------------------------------------------------------- /server/utils/isWookiee.js: -------------------------------------------------------------------------------- 1 | module.exports = isWookiee = (req) => { 2 | return req.encoding === "wookiee"; 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/styles/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | a { 3 | color: #ffffff; 4 | 5 | &:hover { 6 | border-bottom: 1px solid; 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/bogo-text.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/bogo-text.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/dual-grey.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/dual-grey.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/dual-red.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/dual-red.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/gold-black.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/gold-black.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/jedi-hand.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/jedi-hand.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/logo_new.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/logo_new.avif -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/red-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/red-banner.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/dual-silver.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/dual-silver.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/gold-silver.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/gold-silver.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/red-banner-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/red-banner-2.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/red-banner-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/red-banner-3.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/silver-gunsmoke.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/silver-gunsmoke.webp -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/Cinematic_SI_UGC_Tiara.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/Cinematic_SI_UGC_Tiara.avif -------------------------------------------------------------------------------- /client/src/assets/images/saber-masters/silver-black-blue-blades.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semperry/swapi/HEAD/client/src/assets/images/saber-masters/silver-black-blue-blades.webp -------------------------------------------------------------------------------- /client/src/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | $main-text-color: #c8c8c8; 2 | $main-background: #272b30; 3 | $swapi-yellow: #ffe300; 4 | $navbar-height: 50px; 5 | 6 | .yellow { 7 | color: $swapi-yellow; 8 | } -------------------------------------------------------------------------------- /server/middleware/connectDb.js: -------------------------------------------------------------------------------- 1 | const dbConfig = require("../app/dbConfig"); 2 | 3 | async function connectDb(req, res, next) { 4 | await dbConfig(); 5 | next(); 6 | } 7 | 8 | module.exports = connectDb; 9 | -------------------------------------------------------------------------------- /client/src/styles/components/_paypal-button.scss: -------------------------------------------------------------------------------- 1 | #donate-form { 2 | text-align: center; 3 | filter: brightness(50%); 4 | transition: 0.3s ease-in-out all; 5 | 6 | &:hover { 7 | filter: brightness(100%); 8 | } 9 | } -------------------------------------------------------------------------------- /server/routes/rootRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const rootController = require("../controllers/rootController"); 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/", rootController.getRootData); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /server/routes/countRoutes.js: -------------------------------------------------------------------------------- 1 | const countRouter = require("express").Router(); 2 | const countController = require("../controllers/countController"); 3 | 4 | // Get counts 5 | countRouter.get("/all", countController.getCounts); 6 | 7 | module.exports = countRouter; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,jsx,ts,tsx}] 12 | indent_style = tab 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | 4 | import App from "./App.jsx"; 5 | import "./styles/main.scss"; 6 | 7 | createRoot(document.getElementById("root")).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /client/src/styles/pages/_about.scss: -------------------------------------------------------------------------------- 1 | .about-content { 2 | display: grid; 3 | grid-template-columns: 1fr 5fr; 4 | padding-left: 15px; 5 | 6 | a { 7 | color: #ffffff; 8 | &:hover { 9 | border-bottom: 1px solid; 10 | } 11 | } 12 | 13 | pre { 14 | overflow: hidden; 15 | width: 70%; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/middleware/encodingFormat.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | const encodings = ["wookiee", "json"]; 3 | 4 | if (!encodings.includes(req.encoding) && req.encoding) { 5 | res.status(404).json({ message: "Encoding not found" }); 6 | } else { 7 | req.encoding = req.query.format; 8 | next(); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /server/middleware/setUrl.js: -------------------------------------------------------------------------------- 1 | function setUrl(req, res, next) { 2 | try { 3 | const url = `${req.protocol}://${req.headers.host}`; 4 | 5 | req.swapi_url = url; 6 | next(); 7 | } catch (err) { 8 | res 9 | .status(400) 10 | .json({ message: "Swapi Server Error", errors: err.message }); 11 | } 12 | } 13 | 14 | module.exports = setUrl; 15 | -------------------------------------------------------------------------------- /server/models/AdClickModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const clickSchema = new mongoose.Schema({ 4 | adName: { type: String, required: true }, 5 | timestamp: { type: Date, default: Date.now }, 6 | referrer: { type: String }, 7 | userAgent: { type: String }, 8 | }); 9 | 10 | module.exports = mongoose.model("Click", clickSchema); 11 | -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | const proxyAddress = "http://localhost:5000"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | server: { 10 | proxy: { 11 | "/api": proxyAddress, 12 | "/count": proxyAddress, 13 | "/track": proxyAddress, 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.join(__dirname, "../../.env") }); 3 | 4 | const API_BASE_URL = 5 | process.env.NODE_ENV === "production" 6 | ? "https://www.swapi.tech/api" 7 | : "http://localhost:5000/api"; 8 | 9 | const applyConfig = (app) => { 10 | app.set("trust proxy", 1); 11 | }; 12 | 13 | module.exports = { API_BASE_URL, applyConfig }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | nexus/* 26 | 27 | #Environment and config 28 | .env 29 | -------------------------------------------------------------------------------- /server/middleware/checkKey.js: -------------------------------------------------------------------------------- 1 | const checkReportKey = (req, res, next) => { 2 | try { 3 | const { report_key } = req.query; 4 | 5 | if (report_key === process.env.REPORT_KEY) { 6 | next(); 7 | } else { 8 | throw new Error("Unauthorized"); 9 | } 10 | } catch (error) { 11 | return res.status(401).json({ message: error.toString() }); 12 | } 13 | }; 14 | 15 | module.exports = { 16 | checkReportKey, 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // Abstracts 2 | 3 | // Base 4 | @use "base/common"; 5 | 6 | // Components 7 | @use "components/banner-ad"; 8 | @use "components/paypal-button"; 9 | 10 | // Layout 11 | @use "layout/layout"; 12 | @use "layout/navbar"; 13 | @use "layout/swapi-header"; 14 | @use "layout/footer"; 15 | 16 | // Pages 17 | @use "pages/home-page"; 18 | @use "pages/about"; 19 | @use "pages/docs"; 20 | 21 | // Themes 22 | 23 | // Vendors 24 | -------------------------------------------------------------------------------- /client/src/styles/layout/_swapi-header.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts/animations" as *; 2 | 3 | .app-header { 4 | h1 { 5 | font-size: 50px; 6 | font-weight: lighter; 7 | margin-top: 20px; 8 | margin-bottom: 10px; 9 | } 10 | p { 11 | line-height: 1.4; 12 | margin: 0 0 10px; 13 | font-size: 21px; 14 | font-weight: 200; 15 | } 16 | a { 17 | color: #ffffff; 18 | &:hover { 19 | border-bottom: 1px solid; 20 | } 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /client/src/components/SwapiHeader.jsx: -------------------------------------------------------------------------------- 1 | const SwapiHeader = () => { 2 | return ( 3 |
4 |

SWAPI

5 |

The Star Wars API

6 | 7 |

8 | "In my experience, there's no such thing as luck. Big changes are 9 | coming, and we must be prepared."{" "} 10 |

11 | 12 |

Over 3,000,000+ API requests served every day!

13 |
14 | ); 15 | }; 16 | 17 | export default SwapiHeader; 18 | -------------------------------------------------------------------------------- /client/src/styles/layout/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar-container { 2 | 3 | .navlinks-wrapper { 4 | display: flex; 5 | 6 | .navlink { 7 | transition: 0.2s ease-in-out all; 8 | padding: 15px 0px; 9 | border-right: 1px solid rgb(0, 0, 0.2); 10 | border-left: 1px solid rgba(255, 255, 255, 0.1); 11 | &:hover { 12 | background-color: black; 13 | cursor: pointer; 14 | } 15 | 16 | a { 17 | padding: 10px 15px; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/services/rootService.js: -------------------------------------------------------------------------------- 1 | // Get all root info 2 | const getRootData = ({ swapi_url }) => { 3 | try { 4 | const baseUrl = swapi_url + "/api"; 5 | 6 | return { 7 | films: `${baseUrl}/films`, 8 | people: `${baseUrl}/people`, 9 | planets: `${baseUrl}/planets`, 10 | species: `${baseUrl}/species`, 11 | starships: `${baseUrl}/starships`, 12 | vehicles: `${baseUrl}/vehicles`, 13 | }; 14 | } catch (err) { 15 | throw err; 16 | } 17 | }; 18 | 19 | module.exports = { 20 | getRootData, 21 | }; 22 | -------------------------------------------------------------------------------- /server/middleware/limiters.js: -------------------------------------------------------------------------------- 1 | const rateLimit = require("express-rate-limit"); 2 | const slowDown = require("express-slow-down"); 3 | 4 | const apiLimiter = rateLimit({ 5 | windowMs: 15 * 60 * 1000, 6 | max: 100, 7 | message: 8 | "API requests ain't like dusting crops, kid! The nav computer needs a moment, try again soon.", 9 | }); 10 | 11 | const apiSlowDown = slowDown({ 12 | windowMs: 15 * 60 * 1000, 13 | delayAfter: 5, 14 | delayMs: (hits) => hits * 100, 15 | }); 16 | 17 | module.exports = { 18 | apiLimiter, 19 | apiSlowDown, 20 | }; 21 | -------------------------------------------------------------------------------- /server/config/dbConfig.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const MONGODB_URI = 4 | process.env.NODE_ENV === "production" 5 | ? process.env.MONGODB_URI 6 | : "mongodb://127.0.0.1:27017/swapi"; 7 | 8 | module.exports = () => { 9 | mongoose 10 | .connect(MONGODB_URI) 11 | .then(() => console.log("Connected to SWAPI DB")) 12 | .catch((err) => { 13 | console.log("Error connecting: ", err); 14 | }); 15 | 16 | mongoose.connection.on("error", (err) => 17 | console.log("Error after successful connection: ", err), 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /server/services/filmService.js: -------------------------------------------------------------------------------- 1 | const Films = require("../models/FilmModel"); 2 | 3 | // Get All 4 | const getAllFilms = async () => { 5 | try { 6 | const films = await Films.find(); 7 | 8 | return films; 9 | } catch (error) { 10 | throw error; 11 | } 12 | }; 13 | 14 | // Get by ID 15 | const getFilmById = async (id) => { 16 | try { 17 | const film = await Films.findOne({ uid: id }); 18 | 19 | return film; 20 | } catch (error) { 21 | throw error; 22 | } 23 | }; 24 | 25 | module.exports = { 26 | getAllFilms, 27 | getFilmById, 28 | }; 29 | -------------------------------------------------------------------------------- /server/routes/adClickRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const adClickController = require("../controllers/adClickController"); 4 | const checkKey = require("../middleware/checkKey"); 5 | 6 | const adRouter = express.Router(); 7 | 8 | // Get Ads.txt 9 | adRouter.get("/ads.txt", adClickController.getAdsTxt); 10 | 11 | // Create Click 12 | adRouter.get("/saber-masters/:originType", adClickController.addClick); 13 | 14 | // Get Clicks 15 | adRouter.get("/clicks", checkKey.checkReportKey, adClickController.getClicks); 16 | 17 | module.exports = adRouter; 18 | -------------------------------------------------------------------------------- /server/controllers/countController.js: -------------------------------------------------------------------------------- 1 | const countService = require("../services/countService"); 2 | 3 | const getCounts = async (_, res) => { 4 | try { 5 | const counts = await countService.getAllCounts(); 6 | 7 | if (!counts) return res.status(404).json({ message: "Counts not found" }); 8 | 9 | return res.status(200).json({ message: "ok", counts }); 10 | } catch (error) { 11 | console.error(`Get Counts Error: ${error}`); 12 | 13 | return res 14 | .status(400) 15 | .json({ message: "Could not GET counts", errors: `${error}` }); 16 | } 17 | }; 18 | 19 | module.exports = { 20 | getCounts, 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/styles/components/_banner-ad.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts/variables" as *; 2 | 3 | .sticky-ad-bar { 4 | .banner-ad-box { 5 | cursor: pointer; 6 | display: flex; 7 | align-items: center; 8 | width: 30%; 9 | height: 100%; 10 | text-align: center; 11 | 12 | span:last-of-type { 13 | padding-left: 25px; 14 | } 15 | } 16 | 17 | .banner-images-container { 18 | position: relative; 19 | top: 0; 20 | height: 100%; 21 | width: 50%; 22 | left: 25px; 23 | 24 | img { 25 | width: 100%; 26 | height: 100%; 27 | object-fit: contain; 28 | object-position: center; 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /client/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |
5 | Originially by{" "} 6 | 11 | Paul Hallett 12 | {" "} 13 | | Refactored and Maintained by{" "} 14 | 19 | Ryan Curtis 20 | {" "} 21 | © 22 | {new Date().getFullYear()} 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /server/controllers/rootController.js: -------------------------------------------------------------------------------- 1 | const rootService = require("../services/rootService"); 2 | 3 | const getRootData = (req, res) => { 4 | try { 5 | const apiRootData = rootService.getRootData(req); 6 | 7 | if (!apiRootData) 8 | return res.status(404).json({ message: "Root API data not found" }); 9 | 10 | return res.status(200).json({ message: "ok", result: apiRootData }); 11 | } catch (error) { 12 | console.error(`Get API Root Error: ${error}`); 13 | 14 | return res 15 | .status(400) 16 | .json({ message: "Could not GET root data", errors: `${error}` }); 17 | } 18 | }; 19 | 20 | module.exports = { 21 | getRootData, 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | 3 | const NavBar = () => { 4 | return ( 5 |
6 |
React SWAPI
7 |
8 |
9 | 10 | Home 11 | 12 |
13 |
14 | About 15 |
16 |
17 | Documentation 18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default NavBar; 25 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "heroku-postbuild": "cd ../client && npm install && npm run build", 9 | "dev": "nodemon index.js", 10 | "seed": "node scripts/seedDatabase.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.4.7", 18 | "express": "^4.21.2", 19 | "express-rate-limit": "^7.5.0", 20 | "express-slow-down": "^2.0.3", 21 | "mongoose": "^8.10.1" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^3.1.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/services/countService.js: -------------------------------------------------------------------------------- 1 | const models = ([People, Planets, Films, Species, Vehicles, Starships] = [ 2 | require("../models/PeopleModel"), 3 | require("../models/PlanetModel"), 4 | require("../models/FilmModel"), 5 | require("../models/SpeciesModel"), 6 | require("../models/VehicleModel"), 7 | require("../models/StarshipModel"), 8 | ]); 9 | 10 | const getAllCounts = async () => { 11 | const counts = {}; 12 | 13 | try { 14 | for (const model of models) { 15 | const count = await model.countDocuments(); 16 | 17 | counts[model.modelName] = count; 18 | } 19 | 20 | return counts; 21 | } catch (error) { 22 | throw error; 23 | } 24 | }; 25 | 26 | module.exports = { 27 | getAllCounts, 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | /** 4 | * Debounce Hook - Delays updating the value until after a specified delay. 5 | * @param {any} value - The value to debounce. 6 | * @param {number} delay - The debounce delay in milliseconds. 7 | * @returns {any} - The debounced value. 8 | */ 9 | const useDebounce = (value, delay = 500) => { 10 | const [debouncedValue, setDebouncedValue] = useState(value); 11 | 12 | useEffect(() => { 13 | const handler = setTimeout(() => { 14 | setDebouncedValue(value); 15 | }, delay); 16 | 17 | return () => { 18 | clearTimeout(handler); 19 | }; 20 | }, [value, delay]); 21 | 22 | return debouncedValue; 23 | }; 24 | 25 | export default useDebounce; 26 | -------------------------------------------------------------------------------- /client/src/styles/abstracts/_animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes glow-pulse { 2 | 0% { 3 | filter: brightness(1); 4 | } 5 | 50% { 6 | filter: brightness(0.5); 7 | } 8 | 9 | 100% { 10 | filter: brightness(1); 11 | } 12 | } 13 | 14 | @keyframes pulse { 15 | 0% { transform: scale(1); opacity: 1; } 16 | 50% { transform: scale(1.1); opacity: 0.8; } 17 | 100% { transform: scale(1); opacity: 1; } 18 | } 19 | 20 | @keyframes slideshow { 21 | 0% { transform: translateX(0); } 22 | 100% { transform: translateX(-100%); } 23 | } 24 | 25 | .attack { 26 | animation: slideshow 15s linear infinite; 27 | } 28 | 29 | .pulsing { 30 | animation: pulse 3s ease-in-out infinite; 31 | } 32 | 33 | .glow-pulse { 34 | animation: glow-pulse 3s infinite; 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapi-vite", 3 | "version": "1.0.0", 4 | "description": "Root package.json for swapi client and server.", 5 | "main": "eslint.config.js", 6 | "scripts": { 7 | "dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"", 8 | "dev:server": "npm run dev --prefix server", 9 | "dev:client": "npm run dev --prefix client", 10 | "build": "npm run build --prefix client", 11 | "start": "npm run start --prefix server", 12 | "heroku-postbuild": "npm install --prefix server && npm install --prefix client && npm run build --prefix client" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "concurrently": "^9.1.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapi-vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1", 15 | "react-router-dom": "^5.3.4", 16 | "vite": "^6.2.0" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.19.0", 20 | "@types/react": "^19.0.8", 21 | "@types/react-dom": "^19.0.3", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "eslint": "^9.19.0", 24 | "eslint-plugin-react": "^7.37.4", 25 | "eslint-plugin-react-hooks": "^5.0.0", 26 | "eslint-plugin-react-refresh": "^0.4.18", 27 | "globals": "^15.14.0", 28 | "sass": "^1.85.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Switch, Route } from "react-router-dom"; 2 | 3 | import AboutPage from "./pages/AboutPage"; 4 | import AdClickRedirectPage from "./pages/AdClickRedirectPage"; 5 | import DocsPage from "./pages/DocsPage"; 6 | import HomePage from "./pages/HomePage"; 7 | import Layout from "./layouts/Layout"; 8 | 9 | const App = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /server/app/routes.js: -------------------------------------------------------------------------------- 1 | const adClickRoutes = require("../routes/adClickRoutes"); 2 | const countRoutes = require("../routes/countRoutes"); 3 | const filmRoutes = require("../routes/filmRoutes"); 4 | const peopleRoutes = require("../routes/peopleRoutes"); 5 | const planetRoutes = require("../routes/planetRoutes"); 6 | const rootRoutes = require("../routes/rootRoutes"); 7 | const speciesRoutes = require("../routes/speciesRoutes"); 8 | const starshipRoutes = require("../routes/starshipRoutes"); 9 | const vehicleRoutes = require("../routes/vehicleRoutes"); 10 | 11 | const applyRoutes = (app) => { 12 | app.use("/", adClickRoutes); 13 | app.use("/api", [ 14 | rootRoutes, 15 | filmRoutes, 16 | peopleRoutes, 17 | planetRoutes, 18 | speciesRoutes, 19 | starshipRoutes, 20 | vehicleRoutes, 21 | ]); 22 | app.use("/count", countRoutes); 23 | app.use("/track", adClickRoutes); 24 | }; 25 | 26 | module.exports = applyRoutes; 27 | -------------------------------------------------------------------------------- /client/src/components/affiliate/BannerAds.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const images = Object.values( 4 | import.meta.glob("../../assets/images/saber-masters/*.{png,jpg,jpeg,webp}", { 5 | eager: true, 6 | }), 7 | ).map((img) => img.default); 8 | 9 | const BannerAds = () => { 10 | const [imageIndex, setImageIndex] = useState(0); 11 | 12 | useEffect(() => { 13 | const intCB = (index) => { 14 | if (index > images.length - 1) { 15 | return 0; 16 | } else { 17 | return index + 1; 18 | } 19 | }; 20 | const interval = setInterval(() => setImageIndex(intCB), 1000); 21 | 22 | return () => clearInterval(interval); 23 | }, []); 24 | 25 | return ( 26 |
30 | Saber Masters deal 31 |
32 | ); 33 | }; 34 | 35 | export default BannerAds; 36 | -------------------------------------------------------------------------------- /server/app/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | require("dotenv").config({ path: path.join(__dirname, "../../.env") }); 4 | 5 | const dbConfig = require("../config/dbConfig"); 6 | const applyMiddleware = require("./middleware"); 7 | const applyRoutes = require("./routes"); 8 | const { applyConfig } = require("../config/config"); 9 | 10 | const app = express(); 11 | const PORT = process.env.PORT || 5000; 12 | 13 | dbConfig(); 14 | 15 | applyConfig(app); 16 | applyMiddleware(app); 17 | applyRoutes(app); 18 | 19 | if (process.env.NODE_ENV === "production") { 20 | app.use(express.static(path.join(__dirname, "../../client/dist"))); 21 | 22 | app.get("*", (req, res) => { 23 | res.sendFile(path.resolve(__dirname, "../../client/dist", "index.html")); 24 | }); 25 | } 26 | 27 | const startServer = () => { 28 | app.listen(PORT, () => { 29 | console.log(`Server running on port ${PORT}`); 30 | }); 31 | }; 32 | 33 | module.exports = { startServer, app }; 34 | -------------------------------------------------------------------------------- /server/app/middleware.js: -------------------------------------------------------------------------------- 1 | const cors = require("cors"); 2 | const express = require("express"); 3 | 4 | const addAdURL = require("../middleware/addAdURL"); 5 | const setEncoding = require("../middleware/encodingFormat"); 6 | const setUrl = require("../middleware/setUrl"); 7 | const { apiLimiter, apiSlowDown } = require("../middleware/limiters"); 8 | 9 | const allowedHeaders = ["GET"]; 10 | 11 | const applyMiddleware = (app) => { 12 | app.use(cors()); 13 | app.use(express.json()); 14 | 15 | app.set("trust proxy", 1); 16 | app.use(cors({ methods: ["GET"] })); 17 | 18 | // Honey Pot middleware to drop unwanted traffic flood 19 | app.use((req, res, next) => { 20 | if (!allowedHeaders.includes(req.method)) { 21 | req.destroy(); 22 | } else { 23 | next(); 24 | } 25 | }); 26 | 27 | // API-Specific Middleware 28 | app.use("/api", [apiLimiter, apiSlowDown, setEncoding, setUrl, addAdURL]); 29 | app.use("/track", [apiLimiter, apiSlowDown]); 30 | }; 31 | 32 | module.exports = applyMiddleware; 33 | -------------------------------------------------------------------------------- /server/services/peopleService.js: -------------------------------------------------------------------------------- 1 | const Paginate = require("../helpers/pagination"); 2 | const People = require("../models/PeopleModel"); 3 | 4 | // Get All 5 | const getAllPeople = async (req, page, limit) => { 6 | try { 7 | const total = await People.countDocuments(); 8 | const { pageNumber, resultLimit } = Paginate.parseSkip(page, limit, total); 9 | const peoplePagination = new Paginate(req, pageNumber, resultLimit, total); 10 | const pager = peoplePagination.paginate(); 11 | 12 | const people = await People.find( 13 | {}, 14 | {}, 15 | { ...peoplePagination.query, sort: { _id: 1 } }, 16 | ); 17 | 18 | return { pager, people }; 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; 23 | 24 | // Get by ID 25 | const getPersonById = async (id) => { 26 | try { 27 | const person = await People.findOne({ uid: id }); 28 | 29 | return person; 30 | } catch (error) { 31 | throw error; 32 | } 33 | }; 34 | 35 | module.exports = { 36 | getAllPeople, 37 | getPersonById, 38 | }; 39 | -------------------------------------------------------------------------------- /server/services/planetService.js: -------------------------------------------------------------------------------- 1 | const Paginate = require("../helpers/pagination"); 2 | const Planets = require("../models/PlanetModel"); 3 | 4 | // Get All 5 | const getAllPlanets = async (req, page, limit) => { 6 | try { 7 | const total = await Planets.countDocuments(); 8 | const { pageNumber, resultLimit } = Paginate.parseSkip(page, limit, total); 9 | const planetPagination = new Paginate(req, pageNumber, resultLimit, total); 10 | const pager = planetPagination.paginate(); 11 | 12 | const planets = await Planets.find( 13 | {}, 14 | {}, 15 | { ...planetPagination.query, sort: { _id: 1 } }, 16 | ); 17 | 18 | return { pager, planets }; 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; 23 | 24 | // Get by ID 25 | const getPlanetById = async (id) => { 26 | try { 27 | const planet = await Planets.findOne({ uid: id }); 28 | 29 | return planet; 30 | } catch (error) { 31 | throw error; 32 | } 33 | }; 34 | 35 | module.exports = { 36 | getAllPlanets, 37 | getPlanetById, 38 | }; 39 | -------------------------------------------------------------------------------- /server/services/speciesService.js: -------------------------------------------------------------------------------- 1 | const Paginate = require("../helpers/pagination"); 2 | const Species = require("../models/SpeciesModel"); 3 | 4 | // Get All 5 | const getAllSpecies = async (req, page, limit) => { 6 | try { 7 | const total = await Species.countDocuments(); 8 | const { pageNumber, resultLimit } = Paginate.parseSkip(page, limit, total); 9 | const speciesPagination = new Paginate(req, pageNumber, resultLimit, total); 10 | const pager = speciesPagination.paginate(); 11 | 12 | const species = await Species.find( 13 | {}, 14 | {}, 15 | { ...speciesPagination.query, sort: { _id: 1 } }, 16 | ); 17 | 18 | return { species, pager }; 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; 23 | 24 | // Get by ID 25 | const getSpeciesById = async (id) => { 26 | try { 27 | const species = await Species.findOne({ uid: id }); 28 | 29 | return species; 30 | } catch (error) { 31 | throw error; 32 | } 33 | }; 34 | 35 | module.exports = { 36 | getAllSpecies, 37 | getSpeciesById, 38 | }; 39 | -------------------------------------------------------------------------------- /server/services/vehicleService.js: -------------------------------------------------------------------------------- 1 | const Paginate = require("../helpers/pagination"); 2 | const VehicleModel = require("../models/VehicleModel"); 3 | 4 | // Get All 5 | const getAllVehicles = async (req, page, limit) => { 6 | try { 7 | const total = await VehicleModel.countDocuments(); 8 | const { pageNumber, resultLimit } = Paginate.parseSkip(page, limit, total); 9 | const vehiclePagination = new Paginate(req, pageNumber, resultLimit, total); 10 | const pager = vehiclePagination.paginate(); 11 | 12 | const vehicles = await VehicleModel.find( 13 | {}, 14 | {}, 15 | { ...vehiclePagination.query, sort: { _id: 1 } }, 16 | ); 17 | 18 | return { vehicles, pager }; 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; 23 | 24 | // Get by ID 25 | const getVehicleById = async (id) => { 26 | try { 27 | const vehicle = await VehicleModel.findOne({ uid: id }); 28 | 29 | return vehicle; 30 | } catch (error) { 31 | throw error; 32 | } 33 | }; 34 | 35 | module.exports = { 36 | getAllVehicles, 37 | getVehicleById, 38 | }; 39 | -------------------------------------------------------------------------------- /server/services/adClickService.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const Click = require("../models/AdClickModel"); 5 | 6 | const getAdsTxt = () => { 7 | try { 8 | const adsPath = path.join(__dirname, "../../ads.txt"); // adjust as needed 9 | const file = fs.readFileSync(adsPath, "utf8"); 10 | 11 | return file; 12 | } catch (error) { 13 | console.error("Error reading ads.txt:", error); 14 | throw error; 15 | } 16 | }; 17 | 18 | const addClick = async (referrer, userAgent, type) => { 19 | try { 20 | const newClick = await Click.create({ 21 | adName: `Saber Masters: ${type}`, 22 | timestamp: new Date(), 23 | referrer, 24 | userAgent, 25 | }); 26 | 27 | return newClick; 28 | } catch (error) { 29 | throw error; 30 | } 31 | }; 32 | 33 | const getClicks = async () => { 34 | try { 35 | const allClicks = await Click.find(); 36 | 37 | return allClicks; 38 | } catch (error) { 39 | throw error; 40 | } 41 | }; 42 | 43 | module.exports = { 44 | addClick, 45 | getAdsTxt, 46 | getClicks, 47 | }; 48 | -------------------------------------------------------------------------------- /server/services/starshipService.js: -------------------------------------------------------------------------------- 1 | const Paginate = require("../helpers/pagination"); 2 | const StarshipModel = require("../models/StarshipModel"); 3 | 4 | // Get All 5 | const getAllStarships = async (req, page, limit) => { 6 | try { 7 | const total = await StarshipModel.countDocuments(); 8 | const { pageNumber, resultLimit } = Paginate.parseSkip(page, limit, total); 9 | const starshipPagination = new Paginate( 10 | req, 11 | pageNumber, 12 | resultLimit, 13 | total, 14 | ); 15 | const pager = starshipPagination.paginate(); 16 | 17 | const starships = await StarshipModel.find( 18 | {}, 19 | {}, 20 | { ...starshipPagination.query, sort: { _id: 1 } }, 21 | ); 22 | 23 | return { pager, starships }; 24 | } catch (error) { 25 | throw error; 26 | } 27 | }; 28 | 29 | // Get By ID 30 | const getStarshipById = async (id) => { 31 | try { 32 | const starship = await StarshipModel.findOne({ uid: id }); 33 | 34 | return starship; 35 | } catch (error) { 36 | throw error; 37 | } 38 | }; 39 | 40 | module.exports = { 41 | getAllStarships, 42 | getStarshipById, 43 | }; 44 | -------------------------------------------------------------------------------- /server/utils/cache.js: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // Set Cache for all routes (result should be results, or do we streamline?) 3 | const isWookiee = require("./isWookiee"); 4 | 5 | class SwapiCache { 6 | constructor() { 7 | this.cache = {}; 8 | this.window = 10 * 60 * 1000; 9 | } 10 | 11 | checkCache = (req, res, next) => { 12 | if (isWookiee) { 13 | return next(); 14 | } 15 | const routePath = req.route.path.split("/")[1]; 16 | const id = req.params.id; 17 | 18 | if ( 19 | this.cache[`${routePath}:${id}`] && 20 | Date.now() < this.cache[`${routePath}:${id}`].time + this.window 21 | ) { 22 | const result = { ...this.cache[`${routePath}:${id}`] }; 23 | delete result.time; 24 | 25 | return res.status(200).json({ message: "ok", result }); 26 | } else { 27 | next(); 28 | } 29 | }; 30 | 31 | setCache = (req, data) => { 32 | const routePath = req.route.path.split("/")[1]; 33 | const id = req.params.id; 34 | 35 | this.cache[`${routePath}:${id}`] = { 36 | ...data, 37 | time: Date.now(), 38 | }; 39 | }; 40 | } 41 | 42 | const cache = new SwapiCache(); 43 | 44 | module.exports = cache; 45 | -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /server/routes/filmRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const filmController = require("../controllers/filmController"); 4 | const withWookiee = require("../utils/wookieeEncoding"); 5 | const { checkCache } = require("../utils/cache"); 6 | const Films = require("../models/FilmModel"); 7 | 8 | const filmRouter = express.Router(); 9 | 10 | // Search 11 | const searchQuery = (req, res, next) => { 12 | if (!req.query.title) { 13 | next(); 14 | } else { 15 | Films.find({ 16 | "properties.title": { $regex: `${req.query.title}`, $options: "i" }, 17 | }) 18 | .then((results) => { 19 | if (results) { 20 | withWookiee(req, res, results); 21 | } else { 22 | return res 23 | .status(404) 24 | .json({ message: "No results, refine your query" }); 25 | } 26 | }) 27 | .catch((err) => 28 | res 29 | .status(400) 30 | .json({ errors: `${err}`, message: "Could not find film" }), 31 | ); 32 | } 33 | }; 34 | 35 | // GET all 36 | filmRouter.get("/films", searchQuery, filmController.getFilms); 37 | 38 | // GET one 39 | filmRouter.get("/films/:id", checkCache, filmController.getFilm); 40 | 41 | module.exports = filmRouter; 42 | -------------------------------------------------------------------------------- /client/src/components/common/LoadingSpinner.jsx: -------------------------------------------------------------------------------- 1 | const LoadingSpinner = () => { 2 | return ( 3 |
10 | 16 | 25 | 31 | 37 | 45 | 46 | 47 |
48 | ); 49 | }; 50 | 51 | export default LoadingSpinner; 52 | -------------------------------------------------------------------------------- /server/routes/planetRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const Planets = require("../models/PlanetModel"); 4 | const planetController = require("../controllers/planetController"); 5 | const withWookiee = require("../utils/wookieeEncoding"); 6 | const { checkCache } = require("../utils/cache"); 7 | 8 | const planetRouter = express.Router(); 9 | 10 | const searchQuery = (req, res, next) => { 11 | if (!req.query.name) { 12 | next(); 13 | } else { 14 | Planets.find({ 15 | "properties.name": { $regex: `${req.query.name}`, $options: "i" }, 16 | }) 17 | .then((results) => { 18 | if (results) { 19 | withWookiee(req, res, results); 20 | } else { 21 | return res 22 | .status(404) 23 | .json({ message: "No results, refine your query" }); 24 | } 25 | }) 26 | .catch((err) => { 27 | return res 28 | .status(400) 29 | .json({ errors: `${err}`, message: "Could not find planet" }); 30 | }); 31 | } 32 | }; 33 | 34 | // GET all 35 | planetRouter.get("/planets", searchQuery, planetController.getPlanets); 36 | 37 | // GET one 38 | planetRouter.get("/planets/:id", checkCache, planetController.getPlanet); 39 | 40 | module.exports = planetRouter; 41 | -------------------------------------------------------------------------------- /server/routes/peopleRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const { checkCache } = require("../utils/cache"); 4 | const People = require("../models/PeopleModel"); 5 | const peopleController = require("../controllers/peopleController"); 6 | const withWookiee = require("../utils/wookieeEncoding"); 7 | 8 | const peopleRouter = express.Router(); 9 | 10 | // Search 11 | const searchQuery = (req, res, next) => { 12 | if (!req.query.name) { 13 | next(); 14 | } else { 15 | People.find({ 16 | "properties.name": { $regex: `${req.query.name}`, $options: "i" }, 17 | }) 18 | .then((results) => { 19 | if (results) { 20 | withWookiee(req, res, results); 21 | } else { 22 | return res 23 | .status(404) 24 | .json({ message: "No results, refine your query" }); 25 | } 26 | }) 27 | .catch((err) => { 28 | return res 29 | .status(400) 30 | .json({ errors: `${err}`, message: "Could not find person" }); 31 | }); 32 | } 33 | }; 34 | 35 | // GET all 36 | peopleRouter.get("/people", searchQuery, peopleController.getPeople); 37 | 38 | // GET one 39 | peopleRouter.get("/people/:id", checkCache, peopleController.getPerson); 40 | 41 | module.exports = peopleRouter; 42 | -------------------------------------------------------------------------------- /server/routes/speciesRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const speciesController = require("../controllers/speciesController"); 4 | const SpeciesModel = require("../models/SpeciesModel"); 5 | const withWookiee = require("../utils/wookieeEncoding"); 6 | const { checkCache } = require("../utils/cache"); 7 | 8 | const speciesRouter = express.Router(); 9 | 10 | // Search 11 | const searchQuery = (req, res, next) => { 12 | if (!req.query.name) { 13 | next(); 14 | } else { 15 | SpeciesModel.find({ 16 | "properties.name": { $regex: `${req.query.name}`, $options: "i" }, 17 | }) 18 | .then((results) => { 19 | if (results) { 20 | withWookiee(req, res, results); 21 | } else { 22 | return res 23 | .status(404) 24 | .json({ message: "No results, refine your query" }); 25 | } 26 | }) 27 | .catch((err) => { 28 | return res 29 | .status(400) 30 | .json({ errors: `${err}`, message: "Could not find specie" }); 31 | }); 32 | } 33 | }; 34 | 35 | // GET all 36 | speciesRouter.get("/species", searchQuery, speciesController.getSpecies); 37 | 38 | // GET one 39 | speciesRouter.get("/species/:id", checkCache, speciesController.getOneSpecies); 40 | 41 | module.exports = speciesRouter; 42 | -------------------------------------------------------------------------------- /client/src/layouts/Layout.jsx: -------------------------------------------------------------------------------- 1 | import BannerAds from "../components/affiliate/BannerAds"; 2 | import Footer from "../components/Footer"; 3 | import Navbar from "../components/Navbar"; 4 | import PayPalButton from "../components/affiliate/PayPalButton"; 5 | import SwapiHeader from "../components/SwapiHeader"; 6 | 7 | const Layout = ({ children }) => { 8 | const trackAndRedirect = () => { 9 | fetch("/track/saber-masters/ad-site-click") 10 | .then(() => { 11 | window.open( 12 | "https://www.sabermasters.com/discount/RYAN47680", 13 | "_blank" 14 | ); 15 | }) 16 | .catch((err) => console.error("Tracking failed: ", err)); 17 | }; 18 | 19 | return ( 20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 | Get $10 Off! 31 | 32 | Get $10 Off! 33 |
34 | 35 | 36 |
37 | 38 |
{children}
39 |
40 |
42 | ); 43 | }; 44 | 45 | export default Layout; 46 | -------------------------------------------------------------------------------- /client/src/pages/AdClickRedirectPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import AtAtSpinner from "../components/common/AtAtSpinner"; 3 | 4 | const AdClickRedirectPage = (props) => { 5 | const originType = props.match.params.originType; 6 | 7 | useEffect(() => { 8 | const abortController = new AbortController(); 9 | const signal = abortController.signal; 10 | 11 | setTimeout( 12 | () => 13 | fetch( 14 | `/track/saber-masters/${ 15 | originType.includes("sabermasters-swapi") ? "api-click" : "ad-click" 16 | }`, 17 | { signal }, 18 | ) 19 | .then(() => { 20 | window.location.replace( 21 | "https://www.sabermasters.com/discount/RYAN47680", 22 | ); 23 | }) 24 | .catch((err) => console.error("Tracking failed: ", err)), 25 | 3000, 26 | ); 27 | 28 | return () => { 29 | abortController.abort(); 30 | }; 31 | }, [originType]); 32 | 33 | return ( 34 |
43 |

Getting your Discount Ready

44 |

45 | 46 | You will be redirected shortly... 47 |

48 |
49 | ); 50 | }; 51 | 52 | export default AdClickRedirectPage; 53 | -------------------------------------------------------------------------------- /server/controllers/filmController.js: -------------------------------------------------------------------------------- 1 | const filmService = require("../services/filmService"); 2 | const withWookiee = require("../utils/wookieeEncoding"); 3 | const { setCache } = require("../utils/cache"); 4 | 5 | // Get All Films 6 | const getFilms = async (req, res) => { 7 | try { 8 | const films = await filmService.getAllFilms(); 9 | 10 | if (!films) { 11 | return res.status(404).json({ message: "Films not found" }); 12 | } 13 | 14 | return withWookiee(req, res, films); 15 | } catch (err) { 16 | console.error("Get Films Error: ", err); 17 | 18 | return res 19 | .status(500) 20 | .json({ message: "Could not GET Films", errors: `${err}` }); 21 | } 22 | }; 23 | 24 | // Get Film by ID 25 | const getFilm = async (req, res) => { 26 | const id = req.params.id; 27 | 28 | try { 29 | const film = await filmService.getFilmById(id); 30 | 31 | if (!film) { 32 | return res.status(404).json({ message: "Film not found" }); 33 | } 34 | 35 | if (!isWookiee(req)) { 36 | setCache(req, film.toObject()); 37 | } 38 | 39 | return withWookiee(req, res, film); 40 | } catch (error) { 41 | console.error("Get Film Error: ", error); 42 | 43 | return res 44 | .status(500) 45 | .json({ message: "Could not GET Film", errors: `${error}` }); 46 | } 47 | }; 48 | 49 | module.exports = { 50 | getFilms, 51 | getFilm, 52 | }; 53 | -------------------------------------------------------------------------------- /client/src/components/affiliate/PayPalButton.jsx: -------------------------------------------------------------------------------- 1 | const phrases = [ 2 | "De Wanna Wanga?", 3 | "Buy the maintainer some blue milk...", 4 | "Death sticks are expensive...", 5 | "A long time ago, in a wallet far far away...", 6 | "Help me herd some Nerfs...", 7 | "Saving for a clone army, help if you can...", 8 | "Even I get boarded sometimes...", 9 | "I need to stop betting on those pod races...", 10 | "You underestimate my spending power...", 11 | ]; 12 | 13 | const PayPalButton = () => { 14 | return ( 15 | 43 | ); 44 | }; 45 | 46 | export default PayPalButton; 47 | -------------------------------------------------------------------------------- /client/src/styles/pages/_docs.scss: -------------------------------------------------------------------------------- 1 | .doc-content { 2 | display: flex; 3 | padding: 0 15px; 4 | a { 5 | color: #ffffff; 6 | &:hover { 7 | border-bottom: 1px solid; 8 | } 9 | } 10 | 11 | .links-sidebar { 12 | padding-right: 15px; 13 | min-width: 20%; 14 | 15 | .list-group { 16 | margin-bottom: 20px; 17 | padding-left: 0; 18 | 19 | .list-group-item { 20 | width: 100%; 21 | display: block; 22 | padding: 10px 15px; 23 | margin-bottom: -1px; 24 | background-color: #32383e; 25 | border: 1px solid rgba(0, 0, 0, 0.6); 26 | } 27 | :first-child { 28 | border-top-right-radius: 4px; 29 | border-top-left-radius: 4px; 30 | } 31 | :last-child { 32 | border-bottom-right-radius: 4px; 33 | border-bottom-left-radius: 4px; 34 | } 35 | } 36 | } 37 | 38 | .documentation-body { 39 | padding-left: 50px; 40 | width: 77%; 41 | 42 | code { 43 | padding: 2px 4px; 44 | font-size: 90%; 45 | color: #c7254e; 46 | background-color: #f9f2f4; 47 | border-radius: 4px; 48 | } 49 | 50 | pre { 51 | overflow: auto; 52 | white-space: pre-wrap; 53 | word-break: break-all; 54 | word-wrap: break-word; 55 | code { 56 | white-space: pre-wrap; 57 | color: #3a3f44; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 17 | 26 | 27 | 29 | 30 | SWAPI - A New Hope 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Paul Hallett 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of swapi nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /client/src/styles/layout/_layout.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts/variables" as *; 2 | 3 | .layout { 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 100vh; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | 12 | .navbar-container { 13 | position: sticky; 14 | top: 0; 15 | z-index: 1000; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | padding: 0 15px; 20 | border: 1px solid rgba(0, 0, 0, 0.6); 21 | background-color: #3a3f44; 22 | min-height: $navbar-height; 23 | } 24 | 25 | .content-wrapper { 26 | flex: 1; 27 | display: flex; 28 | flex-direction: column; 29 | 30 | .hero { 31 | text-align: center; 32 | padding: 48px 0; 33 | background: #1c1e22; 34 | margin-top: 60px; 35 | border: 1px solid rgba(0, 0, 0, 0.6); 36 | border-radius: 6px; 37 | 38 | } 39 | 40 | .sticky-ad-bar { 41 | display: flex; 42 | align-items: center; 43 | position: sticky; 44 | justify-content: space-between; 45 | top: $navbar-height; 46 | border: 1px solid black; 47 | height: 100px; 48 | padding: 10px; 49 | z-index: 999; 50 | margin-bottom: 45px; 51 | background: $main-background; 52 | } 53 | 54 | .page-content { 55 | flex-grow: 1; 56 | } 57 | } 58 | 59 | .footer { 60 | display: flex; 61 | align-items: center; 62 | padding: 0 15px 15px 15px; 63 | background: $main-background; 64 | color: white; 65 | width: 100%; 66 | margin-top: auto; 67 | } 68 | } -------------------------------------------------------------------------------- /server/helpers/pagination.js: -------------------------------------------------------------------------------- 1 | class Pagination { 2 | constructor(req, page, limit, total) { 3 | this.meta = { 4 | req, 5 | page, 6 | limit, 7 | total, 8 | }; 9 | this.query = {}; 10 | } 11 | 12 | paginate = () => { 13 | const { req, page, limit, total } = this.meta; 14 | let { expanded } = req.query; 15 | expanded = expanded === "true" ? expanded : false; 16 | 17 | this.query = { 18 | skip: limit * (page - 1), 19 | limit, 20 | }; 21 | 22 | return { 23 | total_records: total, 24 | total_pages: Math.ceil(total / limit), 25 | previous: 26 | page === 1 27 | ? null 28 | : `${req.swapi_url}/api${req.route.path}?page=${ 29 | page - 1 30 | }&limit=${limit}${expanded ? "&expanded=" + expanded : ""}`, 31 | next: 32 | page >= Math.ceil(total / limit) 33 | ? null 34 | : `${req.swapi_url}/api${req.route.path}?page=${ 35 | page + 1 36 | }&limit=${limit}${expanded ? "&expanded=" + expanded : ""}`, 37 | }; 38 | }; 39 | 40 | static parseSkip = (page, limit, total) => { 41 | const pageNumber = 42 | page && limit 43 | ? parseInt(page) < 1 44 | ? 1 45 | : parseInt(page) > Math.ceil(total / limit) 46 | ? Math.ceil(total / limit) 47 | : parseInt(page) 48 | : 1; 49 | 50 | const resultLimit = 51 | page && limit ? (parseInt(limit) > total ? total : parseInt(limit)) : 10; 52 | 53 | return { pageNumber, resultLimit }; 54 | }; 55 | } 56 | 57 | module.exports = Pagination; 58 | -------------------------------------------------------------------------------- /server/models/PlanetModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Planet = new Schema({ 5 | description: { 6 | type: String, 7 | required: true, 8 | default: "A planet.", 9 | }, 10 | uid: { 11 | type: String, 12 | required: true, 13 | }, 14 | properties: { 15 | name: { 16 | type: String, 17 | required: true, 18 | }, 19 | diameter: { 20 | type: String, 21 | required: true, 22 | default: "Unknown", 23 | }, 24 | rotation_period: { 25 | type: String, 26 | required: true, 27 | default: "Unknown", 28 | }, 29 | orbital_period: { 30 | type: String, 31 | required: true, 32 | default: "Unknown", 33 | }, 34 | gravity: { 35 | type: String, 36 | required: true, 37 | default: "Unknown", 38 | }, 39 | population: { 40 | type: String, 41 | required: true, 42 | default: "Unknown", 43 | }, 44 | climate: { 45 | type: String, 46 | required: true, 47 | default: "Unknown", 48 | }, 49 | terrain: { 50 | type: String, 51 | required: true, 52 | default: "Unkown", 53 | }, 54 | surface_water: { 55 | type: String, 56 | required: true, 57 | default: "Unknown", 58 | }, 59 | url: { 60 | type: String, 61 | required: true, 62 | }, 63 | created: { 64 | type: Date, 65 | default: Date.now(), 66 | }, 67 | edited: { 68 | type: Date, 69 | default: Date.now(), 70 | }, 71 | }, 72 | }); 73 | 74 | module.exports = mongoose.model("planets", Planet); 75 | -------------------------------------------------------------------------------- /server/controllers/adClickController.js: -------------------------------------------------------------------------------- 1 | const adClickService = require("../services/adClickService"); 2 | 3 | const getAdsTxt = (req, res) => { 4 | try { 5 | const adTxtFile = adClickService.getAdsTxt(); 6 | console.log(adTxtFile); 7 | 8 | res.setHeader("Content-Type", "text/plain"); 9 | return res.status(200).send(adTxtFile); 10 | } catch (error) { 11 | console.error("Error reading ads.txt:", error); 12 | res.status(500).send("Unable to serve ads.txt"); 13 | } 14 | }; 15 | 16 | const addClick = async (req, res) => { 17 | const [referrer, userAgent] = [req.get("Referrer"), req.get("User-Agent")]; 18 | const originType = req.params.originType; 19 | 20 | try { 21 | const newClick = await adClickService.addClick( 22 | referrer, 23 | userAgent, 24 | originType 25 | ); 26 | 27 | return res.status(200).json({ message: "Click tracked", click: newClick }); 28 | } catch (error) { 29 | console.error("Tracking Error: ", error); 30 | 31 | return res 32 | .status(400) 33 | .json({ message: "Could not track click", error: error.toString() }); 34 | } 35 | }; 36 | 37 | const getClicks = async (_, res) => { 38 | try { 39 | const allClicks = await adClickService.getClicks(); 40 | 41 | return res.status(200).json({ message: "Ok", clicks: allClicks }); 42 | } catch (error) { 43 | return res 44 | .status(400) 45 | .json({ message: "something went wrong", error: error.toString() }); 46 | } 47 | }; 48 | 49 | module.exports = { 50 | addClick, 51 | getAdsTxt, 52 | getClicks, 53 | }; 54 | -------------------------------------------------------------------------------- /server/routes/vehicleRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const isWookiee = require("../utils/isWookiee"); 4 | const vehicleController = require("../controllers/vehicleController"); 5 | const VehicleModel = require("../models/VehicleModel"); 6 | const withWookiee = require("../utils/wookieeEncoding"); 7 | const { checkCache, setCache } = require("../utils/cache"); 8 | 9 | const vehicleRouter = express.Router(); 10 | 11 | // Search 12 | const searchQuery = (req, res, next) => { 13 | const { name, model } = req.query; 14 | 15 | if (!name && !model) { 16 | next(); 17 | } else { 18 | VehicleModel.find({ 19 | $or: [ 20 | { 21 | "properties.name": { $regex: `${name}`, $options: "i" }, 22 | }, 23 | { 24 | "properties.model": { 25 | $regex: `${model}`, 26 | $options: "i", 27 | }, 28 | }, 29 | ], 30 | }) 31 | .then((results) => { 32 | if (results) { 33 | withWookiee(req, res, results); 34 | } else { 35 | return res 36 | .status(404) 37 | .json({ message: "No results, refine your query" }); 38 | } 39 | }) 40 | .catch((err) => { 41 | return res 42 | .status(400) 43 | .json({ errors: `${err}`, message: "Could not find vehicle" }); 44 | }); 45 | } 46 | }; 47 | 48 | // GET all 49 | vehicleRouter.get("/vehicles", searchQuery, vehicleController.getVehicles); 50 | 51 | // GET one 52 | vehicleRouter.get("/vehicles/:id", checkCache, vehicleController.getVehicle); 53 | 54 | module.exports = vehicleRouter; 55 | -------------------------------------------------------------------------------- /server/models/FilmModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Film = new Schema({ 5 | uid: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | description: { 11 | type: String, 12 | default: "A Star Wars Film", 13 | }, 14 | properties: { 15 | title: { 16 | type: String, 17 | required: true, 18 | }, 19 | episode_id: { 20 | type: Number, 21 | required: true, 22 | }, 23 | opening_crawl: { 24 | type: String, 25 | required: true, 26 | }, 27 | director: { 28 | type: String, 29 | required: true, 30 | }, 31 | producer: { 32 | type: String, 33 | required: true, 34 | }, 35 | release_date: { 36 | type: String, 37 | required: true, 38 | }, 39 | characters: { 40 | type: Array, 41 | default: [], 42 | required: true, 43 | }, 44 | planets: { 45 | type: Array, 46 | default: [], 47 | required: true, 48 | }, 49 | starships: { 50 | type: Array, 51 | default: [], 52 | required: true, 53 | }, 54 | vehicles: { 55 | type: Array, 56 | default: [], 57 | required: true, 58 | }, 59 | species: { 60 | type: Array, 61 | default: [], 62 | required: true, 63 | }, 64 | url: { 65 | type: String, 66 | required: true, 67 | }, 68 | created: { 69 | type: Date, 70 | default: Date.now(), 71 | }, 72 | edited: { 73 | type: Date, 74 | default: Date.now(), 75 | }, 76 | }, 77 | }); 78 | 79 | module.exports = mongoose.model("films", Film); 80 | -------------------------------------------------------------------------------- /server/middleware/addAdURL.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.join(__dirname, "../../.env") }); 3 | 4 | const addAdURL = (_, res, next) => { 5 | const originalJson = res.json; 6 | const url = 7 | process.env.NODE_ENV === "production" 8 | ? "https://www.swapi.tech" 9 | : "http://localhost:5173"; 10 | 11 | res.json = (data) => { 12 | data.apiVersion = "1.0"; 13 | data.timestamp = new Date(); 14 | data.support = { 15 | contact: "admin@swapi.tech", 16 | donate: 17 | "https://www.paypal.com/donate/?business=2HGAUVTWGR5T2&no_recurring=0&item_name=Support+Swapi+and+keep+the+galaxy%27s+data+free%21+Your+donation+fuels+open-source+innovation+and+helps+us+grow.+Thank+you%21+%F0%9F%9A%80¤cy_code=USD", 18 | partnerDiscounts: { 19 | saberMasters: { 20 | link: `${url}/partner-discount/sabermasters-swapi`, 21 | details: "Use this link to automatically get $10 off your purchase!", 22 | }, 23 | heartMath: { 24 | link: "https://www.heartmath.com/ryanc", 25 | details: "Looking for some Jedi-like inner peace? Take 10% off your heart-brain coherence tools from the HeartMath Institute!" 26 | }, 27 | }, 28 | }; 29 | data.social = { 30 | discord: "https://discord.gg/zWvA6GPeNG", 31 | reddit: "https://www.reddit.com/r/SwapiOfficial/", 32 | github: "https://github.com/semperry/swapi/blob/main/CONTRIBUTORS.md", 33 | }; 34 | 35 | originalJson.call(res, data); 36 | }; 37 | 38 | next(); 39 | }; 40 | 41 | module.exports = addAdURL; 42 | -------------------------------------------------------------------------------- /server/routes/starshipRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | 3 | const isWookiee = require("../utils/isWookiee"); 4 | const starshipController = require("../controllers/starshipController"); 5 | const StarshipModel = require("../models/StarshipModel"); 6 | const withWookiee = require("../utils/wookieeEncoding"); 7 | const { checkCache } = require("../utils/cache"); 8 | 9 | const starshipRouter = express.Router(); 10 | 11 | // Search 12 | const searchQuery = (req, res, next) => { 13 | const { name, model } = req.query; 14 | 15 | if (!name && !model) { 16 | next(); 17 | } else { 18 | StarshipModel.find({ 19 | $or: [ 20 | { 21 | "properties.name": { $regex: `${name}`, $options: "i" }, 22 | }, 23 | { 24 | "properties.model": { 25 | $regex: `${model}`, 26 | $options: "i", 27 | }, 28 | }, 29 | ], 30 | }) 31 | .then((results) => { 32 | if (results) { 33 | withWookiee(req, res, results); 34 | } else { 35 | return res 36 | .status(404) 37 | .json({ message: "No results, refine your query" }); 38 | } 39 | }) 40 | .catch((err) => { 41 | return res 42 | .status(400) 43 | .json({ errors: `${err}`, message: "Could not find starship" }); 44 | }); 45 | } 46 | }; 47 | 48 | // GET all 49 | starshipRouter.get("/starships", searchQuery, starshipController.getStarships); 50 | 51 | // GET one 52 | starshipRouter.get( 53 | "/starships/:id", 54 | checkCache, 55 | starshipController.getStarship, 56 | ); 57 | 58 | module.exports = starshipRouter; 59 | -------------------------------------------------------------------------------- /client/src/styles/base/_common.scss: -------------------------------------------------------------------------------- 1 | @use "../abstracts/variables" as *; 2 | 3 | html, body { 4 | max-width: 100vw; 5 | overflow-x: hidden; 6 | } 7 | 8 | html { 9 | scroll-behavior: smooth; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | font-size: 14px; 16 | line-height: 1.42857142; 17 | color: $main-text-color; 18 | background-color: $main-background; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | a { 24 | text-decoration: none; 25 | color: $main-text-color; 26 | } 27 | 28 | hr { 29 | margin-top: 20px; 30 | margin-bottom: 20px; 31 | border: 0; 32 | border-top: 1px solid #1c1e22; 33 | } 34 | 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5 { 40 | font-size: 18px; 41 | margin: 10px 0; 42 | font-weight: 500; 43 | line-height: 1.1; 44 | color: inherit; 45 | } 46 | 47 | h1 { 48 | font-size: 36px; 49 | margin: 20px 0 10px 0; 50 | } 51 | 52 | h2 { 53 | font-size: 30px; 54 | margin: 20px 0 10px 0; 55 | } 56 | 57 | h3 { 58 | font-size: 24px; 59 | margin-top: 20px; 60 | margin-bottom: 10px; 61 | } 62 | 63 | h5 { 64 | font-size: 14px; 65 | } 66 | 67 | pre { 68 | padding: 9.5px; 69 | margin: 0 0 10px; 70 | font-size: 13px; 71 | line-height: 1.42857143; 72 | word-break: break-all; 73 | word-wrap: break-word; 74 | color: #3a3f44; 75 | background-color: #f5f5f5; 76 | border: 1px solid #cccccc; 77 | border-radius: 4px; 78 | max-height: 340px; 79 | overflow-y: scroll; 80 | text-align: left; 81 | } 82 | 83 | code, 84 | kbd, 85 | pre, 86 | samp { 87 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 88 | } 89 | 90 | p { 91 | margin: 0 0 10px; 92 | } 93 | -------------------------------------------------------------------------------- /server/models/SpeciesModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Species = new Schema({ 5 | description: { 6 | type: String, 7 | default: "A sepcies within the Star Wars universe", 8 | }, 9 | uid: { 10 | type: String, 11 | required: true, 12 | }, 13 | properties: { 14 | name: { 15 | type: String, 16 | required: true, 17 | }, 18 | classification: { 19 | type: String, 20 | required: true, 21 | default: "Unkown", 22 | }, 23 | designation: { 24 | type: String, 25 | required: true, 26 | default: "Unkown", 27 | }, 28 | average_height: { 29 | type: String, 30 | required: true, 31 | default: "Unkown", 32 | }, 33 | average_lifespan: { 34 | type: String, 35 | required: true, 36 | default: "Unkown", 37 | }, 38 | hair_colors: { 39 | type: String, 40 | required: true, 41 | default: "Unkown", 42 | }, 43 | skin_colors: { 44 | type: String, 45 | required: true, 46 | default: "Unknown", 47 | }, 48 | eye_colors: { 49 | type: String, 50 | required: true, 51 | default: "Unknown", 52 | }, 53 | homeworld: { 54 | type: String, 55 | required: true, 56 | default: "Unkown", 57 | }, 58 | language: { 59 | type: String, 60 | required: true, 61 | default: "Unkown", 62 | }, 63 | people: { 64 | type: Array, 65 | required: true, 66 | default: [], 67 | }, 68 | url: { 69 | type: String, 70 | required: true, 71 | }, 72 | created: { 73 | type: Date, 74 | default: Date.now(), 75 | }, 76 | edited: { 77 | type: Date, 78 | default: Date.now(), 79 | }, 80 | }, 81 | }); 82 | 83 | module.exports = mongoose.model("species", Species); 84 | -------------------------------------------------------------------------------- /server/models/PeopleModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Person = new Schema( 5 | { 6 | description: { 7 | type: String, 8 | required: true, 9 | default: "A person within the Star Wars universe", 10 | }, 11 | uid: { 12 | type: String, 13 | required: true, 14 | }, 15 | properties: { 16 | name: { 17 | type: String, 18 | required: true, 19 | }, 20 | height: { 21 | type: String, 22 | required: true, 23 | default: "Unkown", 24 | }, 25 | mass: { 26 | type: String, 27 | required: true, 28 | default: "Unkown", 29 | }, 30 | hair_color: { 31 | type: String, 32 | required: true, 33 | default: "Unkown", 34 | }, 35 | skin_color: { 36 | type: String, 37 | required: true, 38 | default: "Unkown", 39 | }, 40 | eye_color: { 41 | type: String, 42 | required: true, 43 | default: "Unkown", 44 | }, 45 | birth_year: { 46 | type: String, 47 | required: true, 48 | default: "Unkown", 49 | }, 50 | gender: { 51 | type: String, 52 | required: true, 53 | default: "Unkown", 54 | }, 55 | homeworld: { 56 | type: String, 57 | required: true, 58 | }, 59 | vehicles: { type: [String], default: [] }, 60 | starships: { type: [String], default: [] }, 61 | films: { type: [String], default: [] }, 62 | url: { 63 | type: String, 64 | required: true, 65 | }, 66 | created: { 67 | type: Date, 68 | default: Date.now(), 69 | }, 70 | edited: { 71 | type: Date, 72 | default: Date.now(), 73 | }, 74 | }, 75 | }, 76 | { 77 | minimize: false, 78 | } 79 | ); 80 | 81 | module.exports = mongoose.model("people", Person); 82 | -------------------------------------------------------------------------------- /server/controllers/peopleController.js: -------------------------------------------------------------------------------- 1 | const peopleService = require("../services/peopleService"); 2 | const { setCache } = require("../utils/cache"); 3 | const withWookiee = require("../utils/wookieeEncoding"); 4 | 5 | // Get All 6 | const getPeople = async (req, res) => { 7 | const { page, limit, expanded } = req.query; 8 | 9 | try { 10 | const { people, pager } = await peopleService.getAllPeople( 11 | req, 12 | page, 13 | limit, 14 | ); 15 | 16 | if (!people) return res.status(404).json({ message: "People not found" }); 17 | 18 | return withWookiee(req, res, { 19 | ...pager, 20 | results: 21 | expanded === "true" 22 | ? people 23 | : [ 24 | ...people.map((person) => { 25 | return { 26 | uid: person.uid, 27 | name: person.properties.name, 28 | url: person.properties.url, 29 | }; 30 | }), 31 | ], 32 | }); 33 | } catch (error) { 34 | console.error(`Get People Error: ${error}`); 35 | 36 | return res 37 | .status(400) 38 | .json({ message: "Could not GET people", errors: `${error}` }); 39 | } 40 | }; 41 | 42 | // Get by ID 43 | const getPerson = async (req, res) => { 44 | const id = req.params.id; 45 | 46 | try { 47 | const person = await peopleService.getPersonById(id); 48 | 49 | if (!person) return res.status(404).json({ message: "not found" }); 50 | 51 | if (!isWookiee(req)) { 52 | setCache(req, person.toObject()); 53 | } 54 | 55 | return withWookiee(req, res, person); 56 | } catch (error) { 57 | console.error(`Get Person Error: ${error}`); 58 | 59 | return res 60 | .status(400) 61 | .json({ message: "Could not GET person", errors: `${error}` }); 62 | } 63 | }; 64 | 65 | module.exports = { 66 | getPeople, 67 | getPerson, 68 | }; 69 | -------------------------------------------------------------------------------- /server/utils/wookieeEncoding.js: -------------------------------------------------------------------------------- 1 | const lookup = { 2 | a: "ra", 3 | b: "rh", 4 | c: "oa", 5 | d: "wa", 6 | e: "wo", 7 | f: "ww", 8 | g: "rr", 9 | h: "ac", 10 | i: "ah", 11 | j: "sh", 12 | k: "or", 13 | l: "an", 14 | m: "sc", 15 | n: "wh", 16 | o: "oo", 17 | p: "ak", 18 | q: "rq", 19 | r: "rc", 20 | s: "c", 21 | t: "ao", 22 | u: "hu", 23 | v: "ho", 24 | w: "oh", 25 | x: "k", 26 | y: "ro", 27 | z: "uf", 28 | }; 29 | 30 | const translateWookiee = (data) => { 31 | data = JSON.stringify(data); 32 | let encodedString = ""; 33 | 34 | for (let i in data) { 35 | if (Object.keys(lookup).includes(data[i].toLowerCase())) { 36 | data[i] = data[i].toLowerCase(); 37 | } 38 | 39 | if (lookup[data[i]]) { 40 | encodedString += lookup[data[i]]; 41 | } else { 42 | encodedString += data[i]; 43 | } 44 | } 45 | encodedString = JSON.stringify(encodedString); 46 | 47 | return JSON.parse(encodedString); 48 | }; 49 | 50 | module.exports = withWookiee = (req, res, data) => { 51 | let results; 52 | let pager; 53 | 54 | if (!data.results) { 55 | results = { message: "ok", result: data }; 56 | } else { 57 | pager = { 58 | message: "ok", 59 | total_records: data.total_records, 60 | total_pages: data.total_pages, 61 | previous: data.previous, 62 | next: data.next, 63 | }; 64 | 65 | results = [...data.results]; 66 | } 67 | 68 | const wookieeSpeak = translateWookiee(data); 69 | switch (req.encoding) { 70 | case "wookiee": 71 | return res.status(200).json(wookieeSpeak); 72 | case "json": 73 | return res 74 | .status(200) 75 | .json(!data.results ? results : { ...pager, results }); 76 | default: 77 | return res 78 | .status(200) 79 | .json(!data.results ? results : { ...pager, results }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /server/controllers/vehicleController.js: -------------------------------------------------------------------------------- 1 | const vehicleService = require("../services/vehicleService"); 2 | const { setCache } = require("../utils/cache"); 3 | const withWookiee = require("../utils/wookieeEncoding"); 4 | 5 | // Get All 6 | const getVehicles = async (req, res) => { 7 | const { page, limit, expanded } = req.query; 8 | 9 | try { 10 | const { vehicles, pager } = await vehicleService.getAllVehicles( 11 | req, 12 | page, 13 | limit, 14 | ); 15 | 16 | if (!vehicles) return res.status(404).json({ message: "Not found" }); 17 | 18 | return withWookiee(req, res, { 19 | ...pager, 20 | results: 21 | expanded === "true" 22 | ? vehicles 23 | : [ 24 | ...vehicles.map((vehicle) => { 25 | return { 26 | uid: vehicle.uid, 27 | name: vehicle.properties.name, 28 | url: vehicle.properties.url, 29 | }; 30 | }), 31 | ], 32 | }); 33 | } catch (error) { 34 | console.error(`Could not GET Vehicles: ${error}`); 35 | 36 | return res 37 | .status(400) 38 | .json({ message: "Could not GET vehicles", errors: `${error}` }); 39 | } 40 | }; 41 | 42 | // Get by Id 43 | const getVehicle = async (req, res) => { 44 | const id = req.params.id; 45 | 46 | try { 47 | const vehicle = await vehicleService.getVehicleById(id); 48 | 49 | if (!vehicle) return res.status(404).json({ message: "Not found" }); 50 | 51 | if (!isWookiee(req)) { 52 | setCache(req, vehicle.toObject()); 53 | } 54 | 55 | return withWookiee(req, res, vehicle); 56 | } catch (error) { 57 | console.error(`Get Vehicle Error: ${error}`); 58 | 59 | return res 60 | .status(400) 61 | .json({ message: "Could not GET Vehicle", errors: `${error}` }); 62 | } 63 | }; 64 | 65 | module.exports = { 66 | getVehicles, 67 | getVehicle, 68 | }; 69 | -------------------------------------------------------------------------------- /server/controllers/starshipController.js: -------------------------------------------------------------------------------- 1 | const starshipService = require("../services/starshipService"); 2 | const { setCache } = require("../utils/cache"); 3 | const withWookiee = require("../utils/wookieeEncoding"); 4 | 5 | // Get All 6 | const getStarships = async (req, res) => { 7 | const { page, limit, expanded } = req.query; 8 | 9 | try { 10 | const { pager, starships } = await starshipService.getAllStarships( 11 | req, 12 | page, 13 | limit, 14 | ); 15 | 16 | if (!starships) return res.status(404).json({ message: "Not found" }); 17 | 18 | return withWookiee(req, res, { 19 | ...pager, 20 | results: 21 | expanded === "true" 22 | ? starships 23 | : [ 24 | ...starships.map((starship) => { 25 | return { 26 | uid: starship.uid, 27 | name: starship.properties.name, 28 | url: starship.properties.url, 29 | }; 30 | }), 31 | ], 32 | }); 33 | } catch (error) { 34 | console.error(`Could not GET starhsips: ${error}`); 35 | 36 | return res 37 | .status(400) 38 | .json({ message: "Could not GET starhsips", errors: `${error}` }); 39 | } 40 | }; 41 | 42 | // Get By ID 43 | const getStarship = async (req, res) => { 44 | const id = req.params.id; 45 | 46 | try { 47 | const starship = await starshipService.getStarshipById(id); 48 | 49 | if (!starship) return res.status(404).json({ message: "Not found" }); 50 | 51 | if (!isWookiee(req)) setCache(req, starship.toObject()); 52 | 53 | return withWookiee(req, res, starship); 54 | } catch (error) { 55 | console.error(`Could not GET Starship: ${error}`); 56 | 57 | return res 58 | .status(400) 59 | .json({ message: "Could not GET Starship", errors: `${error}` }); 60 | } 61 | }; 62 | 63 | module.exports = { 64 | getStarships, 65 | getStarship, 66 | }; 67 | -------------------------------------------------------------------------------- /server/controllers/planetController.js: -------------------------------------------------------------------------------- 1 | const isWookiee = require("../utils/isWookiee"); 2 | const planetService = require("../services/planetService"); 3 | const withWookiee = require("../utils/wookieeEncoding"); 4 | const { setCache } = require("../utils/cache"); 5 | 6 | // Get All 7 | const getPlanets = async (req, res) => { 8 | const { page, limit, expanded } = req.query; 9 | 10 | try { 11 | const { planets, pager } = await planetService.getAllPlanets( 12 | req, 13 | page, 14 | limit, 15 | ); 16 | 17 | if (!planets) return res.status(404).json({ message: "Planets not found" }); 18 | 19 | return withWookiee(req, res, { 20 | ...pager, 21 | results: 22 | expanded === "true" 23 | ? planets 24 | : [ 25 | ...planets.map((planet) => { 26 | return { 27 | uid: planet.uid, 28 | name: planet.properties.name, 29 | url: planet.properties.url, 30 | }; 31 | }), 32 | ], 33 | }); 34 | } catch (error) { 35 | console.error(`Get Planets Error: ${error}`); 36 | 37 | return res 38 | .status(400) 39 | .json({ message: "Could not GET Planets", errors: `${error}` }); 40 | } 41 | }; 42 | 43 | // Get by ID 44 | const getPlanet = async (req, res) => { 45 | const id = req.params.id; 46 | 47 | try { 48 | const planet = await planetService.getPlanetById(id); 49 | 50 | if (!planet) return res.status(404).json({ messsage: "Not found" }); 51 | 52 | if (!isWookiee(req)) { 53 | setCache(req, planet.toObject()); 54 | } 55 | 56 | return withWookiee(req, res, planet); 57 | } catch (error) { 58 | console.error(`Get Planet Error: ${error}`); 59 | 60 | return res 61 | .status(400) 62 | .json({ message: "Could not GET planet", error: `${error}` }); 63 | } 64 | }; 65 | 66 | module.exports = { 67 | getPlanets, 68 | getPlanet, 69 | }; 70 | -------------------------------------------------------------------------------- /server/controllers/speciesController.js: -------------------------------------------------------------------------------- 1 | const speciesService = require("../services/speciesService"); 2 | const { setCache } = require("../utils/cache"); 3 | const withWookiee = require("../utils/wookieeEncoding"); 4 | 5 | // Get All 6 | const getSpecies = async (req, res) => { 7 | const { page, limit, expanded } = req.query; 8 | 9 | try { 10 | const { species, pager } = await speciesService.getAllSpecies( 11 | req, 12 | page, 13 | limit, 14 | ); 15 | 16 | if (!species) return res.status(404).json({ message: "Species not found" }); 17 | 18 | return withWookiee(req, res, { 19 | ...pager, 20 | results: 21 | expanded === "true" 22 | ? species 23 | : [ 24 | ...species.map((specimen) => { 25 | return { 26 | uid: specimen.uid, 27 | name: specimen.properties.name, 28 | url: specimen.properties.url, 29 | }; 30 | }), 31 | ], 32 | }); 33 | } catch (error) { 34 | console.error(`Could not GET Species: ${error}`); 35 | 36 | return res 37 | .status(400) 38 | .json({ message: "Could not GET all Species", errors: `${error}` }); 39 | } 40 | }; 41 | 42 | // Get by ID 43 | const getOneSpecies = async (req, res) => { 44 | const id = req.params.id; 45 | 46 | try { 47 | const species = await speciesService.getSpeciesById(id); 48 | 49 | if (!species) return res.status(404).json({ message: "Not found" }); 50 | 51 | if (!isWookiee(req)) { 52 | setCache(req, species.toObject()); 53 | } 54 | 55 | return withWookiee(req, res, species); 56 | } catch (error) { 57 | console.error(`Could not GET Species by ID: ${error}`); 58 | 59 | return res 60 | .status(400) 61 | .json({ message: "Could not GET that Species", errors: `${error}` }); 62 | } 63 | }; 64 | 65 | module.exports = { 66 | getSpecies, 67 | getOneSpecies, 68 | }; 69 | -------------------------------------------------------------------------------- /server/models/VehicleModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Vehicle = new Schema({ 5 | description: { 6 | type: String, 7 | default: "A vehicle", 8 | }, 9 | uid: { 10 | type: String, 11 | required: true, 12 | }, 13 | properties: { 14 | name: { 15 | type: String, 16 | required: true, 17 | }, 18 | model: { 19 | type: String, 20 | required: true, 21 | default: "Unknown", 22 | }, 23 | vehicle_class: { 24 | type: String, 25 | required: true, 26 | default: "Unknown", 27 | }, 28 | manufacturer: { 29 | type: String, 30 | required: true, 31 | default: "Unknown", 32 | }, 33 | cost_in_credits: { 34 | type: String, 35 | required: true, 36 | default: "Unknown", 37 | }, 38 | length: { 39 | type: String, 40 | required: true, 41 | default: "Unknown", 42 | }, 43 | crew: { 44 | type: String, 45 | required: true, 46 | default: "Unknown", 47 | }, 48 | passengers: { 49 | type: String, 50 | required: true, 51 | default: "Unknown", 52 | }, 53 | max_atmosphering_speed: { 54 | type: String, 55 | required: true, 56 | default: "Unknown", 57 | }, 58 | cargo_capacity: { 59 | type: String, 60 | required: true, 61 | default: "Unknown", 62 | }, 63 | consumables: { 64 | type: String, 65 | required: true, 66 | default: "Unknown", 67 | }, 68 | films: { 69 | type: Array, 70 | default: [], 71 | required: true, 72 | }, 73 | pilots: { 74 | type: Array, 75 | default: [], 76 | required: true, 77 | }, 78 | url: { 79 | type: String, 80 | required: true, 81 | }, 82 | created: { 83 | type: Date, 84 | required: true, 85 | default: Date.now(), 86 | }, 87 | edited: { 88 | type: Date, 89 | required: true, 90 | default: Date.now(), 91 | }, 92 | }, 93 | }); 94 | 95 | module.exports = mongoose.model("vehicles", Vehicle); 96 | -------------------------------------------------------------------------------- /server/models/StarshipModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const Starship = new Schema({ 5 | description: { 6 | type: String, 7 | default: "A Starship", 8 | }, 9 | uid: { 10 | type: String, 11 | required: true, 12 | }, 13 | properties: { 14 | name: { 15 | type: String, 16 | required: true, 17 | }, 18 | model: { 19 | type: String, 20 | required: true, 21 | default: "Unknown", 22 | }, 23 | starship_class: { 24 | type: String, 25 | required: true, 26 | default: "Unknown", 27 | }, 28 | manufacturer: { 29 | type: String, 30 | required: true, 31 | default: "Unknown", 32 | }, 33 | cost_in_credits: { 34 | type: String, 35 | required: true, 36 | default: "Unknown", 37 | }, 38 | length: { 39 | type: String, 40 | required: true, 41 | default: "Unknown", 42 | }, 43 | crew: { 44 | type: String, 45 | required: true, 46 | default: "Unknown", 47 | }, 48 | passengers: { 49 | type: String, 50 | required: true, 51 | default: "Unknown", 52 | }, 53 | max_atmosphering_speed: { 54 | type: String, 55 | required: true, 56 | default: "Unknown", 57 | }, 58 | hyperdrive_rating: { 59 | type: String, 60 | required: true, 61 | default: "Unknown", 62 | }, 63 | MGLT: { 64 | type: String, 65 | required: true, 66 | default: "Unknown", 67 | }, 68 | cargo_capacity: { 69 | type: String, 70 | required: true, 71 | default: "Unknown", 72 | }, 73 | consumables: { 74 | type: String, 75 | required: true, 76 | default: "Unknown", 77 | }, 78 | films: { 79 | type: Array, 80 | default: [], 81 | required: true, 82 | }, 83 | pilots: { 84 | type: Array, 85 | default: [], 86 | required: true, 87 | }, 88 | url: { 89 | type: String, 90 | required: true, 91 | }, 92 | created: { 93 | type: Date, 94 | default: Date.now(), 95 | }, 96 | edited: { 97 | type: Date, 98 | default: Date.now(), 99 | }, 100 | }, 101 | }); 102 | 103 | module.exports = mongoose.model("starships", Starship); 104 | -------------------------------------------------------------------------------- /client/src/components/common/AtAtSpinner.jsx: -------------------------------------------------------------------------------- 1 | const AtAtSpinner = (props) => { 2 | return ( 3 | <> 4 | 10 | {/* AT-AT Walker Body */} 11 | 20 | 21 | {/* Head */} 22 | 31 | 32 | {/* Red Eyes */} 33 | 34 | 40 | 41 | 42 | {/* Legs with Walking Animation */} 43 | 44 | 50 | 51 | 52 | 59 | 60 | 61 | 67 | 68 | 69 | 76 | 77 | 78 | {/* Snowspeeder */} 79 | 86 | 94 | 95 | 96 | {/* AT-AT Laser Shot (Aiming Downwards) */} 97 | 98 | 104 | 110 | 111 | 112 |

{props.message}

113 | 114 | ); 115 | }; 116 | 117 | export default AtAtSpinner; 118 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors ✨ 2 | 3 | Swapi is an open-source project made possible by contributions from an amazing community. Below is a list of people who have helped improve and maintain this project in various ways. 4 | 5 | We appreciate all contributions whether it's code, documentation, bug reports, ideas, community support, third-party integrations, or financial support. Want to contribute? Check out our [CONTRIBUTING.md](CONTRIBUTING.md) guide! 6 | 7 | --- 8 | 9 | ## 🔧 Maintainer 10 | 11 | - **[Ryan Curtis](https://github.com/semperry)** – Current maintainer & lead developer 12 | 13 | --- 14 | 15 | ## 🏛️ Original Creators (swapi.co) 16 | 17 | Special thanks to the original team who built swapi.co and laid the foundation for this project: 18 | 19 | - **[Paul Hallett](https://github.com/phalt)** – Creator of swapi.co 20 | 21 | --- 22 | 23 | ## 🚀 Code Contributors 24 | 25 | Those who have directly contributed to swapi.tech's core codebase. 26 | 27 | - **[Frankieali](https://github.com/frankieali)** – 'Expanded' query parameter for full result properties 28 | 29 | --- 30 | 31 | ## 🔌 Integrations & SDKs 32 | 33 | Developers who have built tools, wrappers, or third-party libraries to extend swapi.tech. 34 | 35 | - **[Carrie Mathieu](https://github.com/carriemathieu)** – Built the **Ruby gem [sw_tour](https://github.com/carriemathieu/sw_tour)** for swapi.tech 36 | - **[Ryan Curtis](https://github.com/semperry)** – Created **[react-swapi](https://www.npmjs.com/package/react-swapi)** wrapper for swapi.tech 37 | 38 | --- 39 | 40 | ## 📝 Documentation 41 | 42 | Contributors who improved the API docs, examples, and guides. 43 | 44 | - _(No contributors yet. Want to help? Open an issue or PR!)_ 45 | 46 | ## 🐛 Bug Reports & Testing 47 | 48 | Users who reported bugs or helped with testing. 49 | 50 | - **[Abhay Lokesh](https://github.com/abhay-lokesh)** – Identified a discrepency between documenation and data being returned by the people api 51 | - **[Rodrigo Deodoro](https://github.com/roddds)** – Reported search api break after a library update 52 | 53 | --- 54 | 55 | ## 🌱 Ideas & Suggestions 56 | 57 | People who provided valuable insights, suggestions, or feature requests. 58 | 59 | - _(No contributors yet. Want to help? Open an issue or PR!)_ 60 | 61 | ## 📢 Community & Support 62 | 63 | People who help answer questions, support users, or spread the word about swapi.tech. 64 | 65 | - _(No contributors yet. Want to help? Open an issue or PR!)_ 66 | 67 | --- 68 | 69 | ## 💖 Donors & Supporters 70 | 71 | A huge thank you to those who have financially supported Swapi! Your contributions help keep the project running and improve its development. 72 | 73 | - **[Sean Doyle](https://github.com/HumberSean)** - Supported Swapi financially 74 | - **[Norman MacPherson](https://github.com/leemacpherson)** – Supported Swapi financially 75 | - **[Tom Power](https://github.com/TPower2112)** – Supported Swapi financially 76 | - **[Andrew Wold](https://github.com/Andrewf9001)** – Supported Swapi financially 77 | - _(Donated? Add yourself here!)_ 78 | 79 | Thank you all so much!!! 80 | 81 | --- 82 | 83 | ## Want to be listed here? 84 | 85 | Contributions of all types are welcome! If you've contributed to Swapi in any way and aren't listed, feel free to submit a pull request to be added. 86 | -------------------------------------------------------------------------------- /client/src/styles/pages/_home-page.scss: -------------------------------------------------------------------------------- 1 | .content-container { 2 | text-align: center; 3 | 4 | .input-group { 5 | .input-group-addon { 6 | padding: 8px 12px; 7 | font-size: 14px; 8 | font-weight: normal; 9 | line-height: 1; 10 | color: #272b30; 11 | text-align: center; 12 | background-color: #999999; 13 | border: 1px solid #cccccc; 14 | border-radius: 4px; 15 | } 16 | 17 | .input-group-control { 18 | width: 33%; 19 | padding: 7px 12px; 20 | font-size: 14px; 21 | line-height: 1.42857143; 22 | color: #272b30; 23 | background-color: #ffffff; 24 | border: 1px solid #cccccc; 25 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 26 | transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 27 | } 28 | 29 | .input-group-btn { 30 | background-image: linear-gradient(#8a9196, #7a8288 60%, #70787d); 31 | .btn { 32 | text-align: center; 33 | cursor: pointer; 34 | background-image: none; 35 | border: 1px solid transparent; 36 | padding: 8px 12px; 37 | font-size: 14px; 38 | line-height: 1.42857143; 39 | border-radius: 4px; 40 | border-bottom-left-radius: 0; 41 | border-top-left-radius: 0; 42 | } 43 | 44 | .btn-primary { 45 | background-repeat: no-repeat; 46 | background-color: #7a8288; 47 | 48 | &:hover { 49 | background-image: linear-gradient(#484e55, #3a3f44 60%, #313539); 50 | border-color: rgba(0, 0, 0, 0.6); 51 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); 52 | } 53 | } 54 | } 55 | 56 | :first-child { 57 | border-right: 0; 58 | border-bottom-right-radius: 0; 59 | border-top-right-radius: 0; 60 | background-image: linear-gradient(#484e55, #3a3f44 60%, #313539); 61 | border-color: rgba(0, 0, 0, 0.6); 62 | background-repeat: no-repeat; 63 | color: #ffffff; 64 | } 65 | } 66 | 67 | .example-routes-wrapper { 68 | width: 100%; 69 | position: relative; 70 | right: 15%; 71 | 72 | a { 73 | color: #ffffff; 74 | &:hover { 75 | border-bottom: 1px solid #ffffff; 76 | } 77 | } 78 | } 79 | 80 | .result-header { 81 | position: relative; 82 | right: 24.5%; 83 | padding-top: 34px; 84 | font-weight: 300; 85 | margin-bottom: 20px; 86 | line-height: 1.4; 87 | font-size: 21px; 88 | } 89 | 90 | .json-content { 91 | display: flex; 92 | justify-content: center; 93 | align-items: center; 94 | text-align: center; 95 | min-height: 20px; 96 | 97 | .well { 98 | max-width: 50%; 99 | padding: 19px; 100 | margin-bottom: 20px; 101 | background-color: #1c1e22; 102 | border: 1px solid #0c0d0e; 103 | border-radius: 4px; 104 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 105 | 106 | .react-json-view { 107 | overflow: scroll; 108 | } 109 | } 110 | } 111 | 112 | .bottom-row { 113 | padding-bottom: 34px; 114 | display: grid; 115 | gap: 50px; 116 | grid-template-columns: 1fr 1fr 1fr; 117 | text-align: left; 118 | margin-left: 100px; 119 | margin-right: 100px; 120 | h4 { 121 | text-align: center; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /client/src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import AtAtSpinner from "../components/common/AtAtSpinner"; 5 | import useDebounce from "../hooks/useDebounce"; 6 | 7 | const HomePage = () => { 8 | const [endpoint, setEndpoint] = useState(""); 9 | const [currentData, setCurrentData] = useState(null); 10 | const [isDataLoading, setIsDataLoading] = useState(true); 11 | const debouncedEndpoint = useDebounce(endpoint); 12 | 13 | const handleFetchPreview = useCallback( 14 | (signal) => { 15 | setIsDataLoading(true); 16 | 17 | fetch(`/api/${endpoint}`, { signal }) 18 | .then((res) => res.json()) 19 | .then((data) => { 20 | setCurrentData(data); 21 | setIsDataLoading(false); 22 | }) 23 | .catch((err) => { 24 | console.log(err); 25 | if (!signal.aborted) { 26 | setIsDataLoading(false); 27 | } 28 | }); 29 | }, 30 | [endpoint] 31 | ); 32 | 33 | const handleChange = (e) => { 34 | setIsDataLoading(true); 35 | setEndpoint(e.target.value); 36 | }; 37 | 38 | const renderData = () => { 39 | if (isDataLoading) 40 | return ( 41 | 45 | ); 46 | if (currentData) { 47 | return
{JSON.stringify(currentData, null, 2)}
; 48 | } else { 49 | return null; 50 | } 51 | }; 52 | 53 | useEffect(() => { 54 | const abortController = new AbortController(); 55 | const signal = abortController.signal; 56 | 57 | if (debouncedEndpoint || !currentData) { 58 | handleFetchPreview(signal); 59 | } 60 | 61 | return () => abortController.abort(); 62 | }, [debouncedEndpoint]); 63 | 64 | return ( 65 |
66 |
67 |
68 |

All the Star Wars data you've ever wanted:

69 |

70 | Planets, Spaceships, Vehicles, People, Films and Species 71 |

72 |

Now with The Force Awakens data!

73 |
74 |
75 | 76 |
77 |
78 |

Try it now!

79 |
80 | https://www.swapi.tech/api/ 81 | { 88 | if (e.key === "Enter") handleFetchPreview(); 89 | }} 90 | /> 91 | 92 | 99 | 100 |
101 |
102 | Need a hint? try{" "} 103 | setEndpoint("people/1")}> 104 | people/1/ 105 | {" "} 106 | or{" "} 107 | setEndpoint("planets/3")}> 108 | planets/3/ 109 | {" "} 110 | or{" "} 111 | setEndpoint("starships/9")}> 112 | starships/9/ 113 | {" "} 114 |
115 |

Result:

116 |
117 |
{renderData()}
118 |
119 |
120 | 121 |
122 |
123 |

What is this?

124 |

125 | The Star Wars API, or "swapi" (Swah-pee) is the world's first 126 | quantified and programmatically-accessible data source for all the 127 | data from the Star Wars canon universe! 128 |

129 |

130 | We've taken all the rich contextual stuff from the universe and 131 | formatted into something easier to consume with software. Then we 132 | went and stuck an API on the front so you can access it all! 133 |

134 |
135 |
136 |

How can I use it?

137 |

138 | All the data is accessible through our HTTP web API. Consult our{" "} 139 | documentation if you'd like to get 140 | started. 141 |

142 |
143 |
144 |

What happened with old swapi.co?

145 |

146 | The original swapi.co is not supported or maintained anymore. But 147 | since so many of my projects and tutorials used it, as well as my 148 | colleagues I decided to rebuild it from (almost) scratch. 149 |

150 |
151 |
152 |
153 |
154 | ); 155 | }; 156 | 157 | export default HomePage; 158 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Swapi 2 | 3 | Thank you for your interest in contributing to Swapi! 🚀 This guide will help you get started with making meaningful contributions to the project. 4 | 5 | --- 6 | 7 | ## 🛠 Getting Started 8 | 9 | ### 1. Fork the Repository 10 | 11 | 1. Go to the [Swapi GitHub repository](https://github.com/semperry/swapi). 12 | 2. Click the **Fork** button in the top-right corner. 13 | 3. Clone your forked repository to your local machine: 14 | ```sh 15 | git clone https://github.com/YOUR_GITHUB_USERNAME/swapi.git 16 | cd swapi 17 | ``` 18 | 4. Add the upstream repository to keep your fork updated: 19 | ```sh 20 | git remote add upstream https://github.com/semperry/swapi.git 21 | ``` 22 | 23 | ### 2. Install Dependencies 24 | 25 | Swapi requires **Node.js** and **npm**. Install dependencies using: 26 | 27 | ```sh 28 | npm install 29 | ``` 30 | 31 | You will need to run this in the root directory, as well as both the client and server folders. 32 | 33 | ### 3. Running the Project Locally (Vite + Express) 34 | 35 | Swapi is now a **Vite + Express** project. Use the following commands based on what you need: 36 | 37 | - **Start the development server from the root directory (this spins up both frontend & backend with hot reloading):** 38 | 39 | ```sh 40 | npm run dev 41 | ``` 42 | 43 | - **Frontend runs on** `http://localhost:5173` 44 | - **Backend API runs on** `http://localhost:5000` 45 | - **Requests from :5173 will proxy to :5000** 46 | 47 | - **Run only the backend (Express server with Nodemon):** 48 | 49 | ```sh 50 | npm run dev:server 51 | ``` 52 | 53 | - **Run only the frontend (Vite's hot-reload dev server):** 54 | 55 | ```sh 56 | npm run dev:client 57 | ``` 58 | 59 | - **Build the frontend for production:** 60 | ```sh 61 | npm run build 62 | ``` 63 | 64 | ### 4. MongoDB Setup & Seeding 65 | 66 | Swapi requires a **MongoDB instance** to run locally. 67 | 68 | ## 🌱 Seeding the Database 69 | 70 | Before running the project, you may want to **populate the database** with demo data. 71 | 72 | ### **🛠 Running the Seed Script** 73 | 74 | To seed **all available models**, from inside of the **server directory**, run: 75 | 76 | ```sh 77 | npm run seed 78 | ``` 79 | 80 | 🛠 Seeding Specific Models 81 | You can optionally specify which models to seed: 82 | 83 | ```sh 84 | npm run seed -- people films planets 85 | ``` 86 | 87 | This will only seed the people, films, and planets collections. 88 | 89 | - NOTE: passing starships or vehicles will automatically seed from "transports". 90 | 91 | ⚠️ Confirmation Step 92 | The script will prompt you for confirmation before proceeding with the seeding process. 93 | 94 | 🛠 Resetting the Database 95 | If you need to completely reset the database, you can run: 96 | 97 | ```sh 98 | node resetDB.js 99 | ``` 100 | 101 | 🚀 Now you're ready to contribute! 102 | 103 | --- 104 | 105 | ## 📌 Coding Standards 106 | 107 | - Follow the existing **code style and structure**. 108 | - Use **meaningful commit messages**. 109 | - Write **clear and concise comments** where necessary. 110 | - Ensure code changes **do not break existing functionality**. 111 | - Format code for tabs with a size of 2. 112 | 113 | --- 114 | 115 | ## 🐛 Reporting Issues 116 | 117 | If you find a bug or have a feature request: 118 | 119 | 1. **Search existing issues** to avoid duplicates. 120 | 2. If no similar issue exists, [open a new issue](https://github.com/semperry/swapi/issues/new). 121 | 3. Include **steps to reproduce, expected vs. actual behavior, and environment details (OS, Node.js version, etc.)**. 122 | 123 | --- 124 | 125 | ## 🚀 Submitting Pull Requests (PRs) 126 | 127 | ### 1. Create a Feature Branch 128 | 129 | Always create a new branch from `dev` for your changes: 130 | 131 | ```sh 132 | git checkout dev # Switch to the dev branch 133 | git fetch upstream dev # Fetch the latest dev branch from the main repo 134 | git pull upstream dev # Ensure your local dev branch is up to date 135 | git checkout -b feature-branch-name # Create your feature branch 136 | ``` 137 | 138 | #### ✅ This ensures all features branch from dev, preventing conflicts in main. 139 | 140 | ### 2. Make Your Changes 141 | 142 | - Test your changes locally. 143 | - Ensure new features do not introduce breaking changes. 144 | - If making UI or API changes, update relevant **documentation** in the documentation React component as well as the README file if necessary. 145 | 146 | ### 3. Commit and Push 147 | 148 | ```sh 149 | git add . 150 | git commit -m "Brief description of changes" 151 | git push origin feature-branch-name 152 | ``` 153 | 154 | ### 4. Open a Pull Request 155 | 156 | 1. **Make sure your feature branch is up to date with `dev` from the main repo:** 157 | ```sh 158 | git checkout dev 159 | git fetch upstream dev # Fetch the latest changes 160 | git pull upstream dev # Pull the latest dev into local dev branch 161 | git checkout your-feature-name 162 | git merge dev # Merge the latest dev into your feature branch 163 | ``` 164 | 2. Push your branch to GitHub. If there were conflicts or final changes made, be sure to add and commit first. 165 | ```sh 166 | git push origin your-feature-name 167 | ``` 168 | 3. Go to your forked repository on GitHub. 169 | 4. Click **Compare & pull request**. 170 | 171 | - Base branch: `dev` (not main!) 172 | - Compare branch: your-feature-name 173 | - Fill in a clear **title and description** of your changes. 174 | - Ensure all commits are squashed into a single meaningful commit before submitting the PR. 175 | - Add yourself to the [`CONTRIBUTORS.md`](CONTRIBUTORS.md) file under the appropriate section and in alphabetical order, if you haven't already. Recommit and push if necessary. 176 | - If you have financially supported the project, you may also add yourself under the **Donors & Supporters section** in both this file and the [`CONTRIBUTORS.md`](CONTRIBUTORS.md). 177 | 178 | 9. Submit the PR and wait for review! 179 | 180 | --- 181 | 182 | ## 🔥 Contribution Types & Recognition 183 | 184 | All contributions are recognized in [`CONTRIBUTORS.md`](CONTRIBUTORS.md). You can contribute in multiple ways: 185 | 186 | - 🛠 **Code Contributions**: API improvements, bug fixes, optimizations. 187 | - 📝 **Documentation**: Enhancing API docs, adding examples. 188 | - 🐛 **Bug Reports & Testing**: Finding and reporting issues. 189 | - 🌱 **Ideas & Suggestions**: Proposing new features. 190 | - 🔌 **Third-Party Integrations**: Creating libraries or SDKs. 191 | - 📢 **Community & Support**: Helping others use Swapi. 192 | - 💖 **Donors & Supporters**: Providing financial contributions to keep the project running. 193 | 194 | Want to be listed? Open an issue or PR! 🚀 195 | 196 | --- 197 | 198 | ## ❓ Need Help? 199 | 200 | If you have any questions, feel free to **open a discussion** or reach out via [GitHub Issues](https://github.com/semperry/swapi/issues). Happy coding! 🎉 201 | -------------------------------------------------------------------------------- /client/src/pages/AboutPage.jsx: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // Counts to global state 3 | import { useState, useEffect } from "react"; 4 | 5 | const AboutPage = () => { 6 | const [counts, setCounts] = useState({}); 7 | 8 | const renderStats = () => { 9 | return Object.keys(counts).map((stat, idx) => { 10 | return ( 11 |

12 | {stat[0].toUpperCase() + stat.slice(1)}: {counts[stat]} 13 |

14 | ); 15 | }); 16 | }; 17 | 18 | useEffect(() => { 19 | const abortController = new AbortController(); 20 | const signal = abortController.signal; 21 | 22 | fetch("/count/all", { signal }) 23 | .then((res) => res.json()) 24 | .then((data) => setCounts(data.counts)) 25 | .catch((err) => console.log(err)); 26 | 27 | return () => abortController.abort(); 28 | }, []); 29 | 30 | return ( 31 |
32 |
33 |

Statistics

34 | {counts ? renderStats() : null} 35 |
36 |
37 |

What is this?

38 | 39 |

From the prequel:

40 |

41 | "The Star Wars API is the world's first quantified and 42 | programmatically-formatted set of Star Wars data. 43 |

44 | 45 |

46 | After hours of watching films and trawling through content online, we 47 | present to you all the{" "} 48 | 49 | People, Films, Species, Starships, Vehicles and Planets 50 | {" "} 51 | from Star Wars. 52 |

53 | 54 |

55 | We've formatted this data in{" "} 56 | 57 | JSON 58 | {" "} 59 | and exposed it to you in a{" "} 60 | 61 | RESTish 62 | {" "} 63 | implementation that allows you to programmatically collect and measure 64 | the data." 65 |

66 | 67 |

68 | I have replicated the original SWAPI platform, not only as a challenge 69 | for myself ( the{" "} 70 | 75 | {" "} 76 | Official SWAPI repository{" "} 77 | {" "} 78 | takes you pretty far if you want a clone in Django),
79 | but as a way to provide a standard API for my students without fear of 80 | it being taken down 81 |

82 | 83 |

84 | 85 | Check out the documentation to get started consuming swapi data 86 | 87 |

88 | 89 |

What happened to swapi.co?

90 | 91 |

92 | Swapi.co?? Swapi.co.... That's a name I've not heard in a long time... 93 | A long time. 94 |

95 |

96 | Unfortulately swapi.co is no longer maintained, and the service is 97 | currently down. This is a personalized branch of SWAPI that I have 98 | built to continue in assisting
99 | my students (and others) with api comminucation. I will maintain it as 100 | much as possible. 101 |

102 | 103 |

What can you use this for?

104 | 105 |

106 | Use SWAPI to fetch Star Wars universe data. This api is a fantastic 107 | educational resource. 108 |

109 |

110 | The original swapi had amazing helper libraries that I will steadily 111 | begin implenting. If you have one to contribute, or would like to 112 | contribute, feel free to contact me: inquiry@swapi.tech 113 |

114 | 115 |

116 | Fetch people, planets, vehicles, and more.: 117 |

118 | 119 |
120 | 					
121 | 						fetch('https://www.swapi.tech/api/planets/1')
122 | 						
{`.then(res => res.json())`} 123 |
{`.then(data => console.log(data))`} 124 |
{`.catch(err => console.error(err);`} 125 |
126 |
127 |
128 | 129 |

What are the features?

130 | 131 |

132 | Originially SWAPI used{" "} 133 | 138 | Django{" "} 139 | 140 | and{" "} 141 | 146 | Django REST Framework 147 | {" "} 148 | to serve a{" "} 149 | 154 | RESTish 155 | {" "} 156 | API to you. 157 |

158 |

159 | In this implementation I used{" "} 160 | 165 | React 166 | {" "} 167 | and{" "} 168 | 173 | Express 174 | {" "} 175 | to serve a{" "} 176 | 181 | monolothic 182 | {" "} 183 | style application . The data is all formatted in{" "} 184 | 185 | JSON 186 | 187 | . 188 |

189 | 190 |

Who are you?

191 | 192 |

193 | I am{" "} 194 | 199 | Ryan Curtis 200 | 201 | , Full Stack Web Engineer and CTO. 202 |

203 | 204 |

Original author?

205 | 206 |

207 | This project was originally built and maintained by{" "} 208 | 209 | Paul Hallett 210 | 211 | . 212 |

213 | 214 |

Copyright and stuff?

215 | 216 |

Star Wars and all associated names are copyright Lucasfilm ltd.

217 | 218 |

This project is open source and carries a BSD licence.

219 | 220 |

221 | All data has been freely collected from open sources such as{" "} 222 | 227 | Wookiepedia 228 | 229 | . 230 |

231 | 232 |

233 | All data as of 5/8/2020 was collected by the original contributors. I 234 | will be adding data from the same open sources slowly but surely 235 |

236 | 237 |

Contributors

238 | 239 |

240 | SWAPI would not be possible without contributions from the following 241 | people: 242 |

243 | 244 | 291 | 292 |

293 | I thank you for providing a solid framework to piggy back off of to 294 | continue educating the masses about api comminucation. Thank you. 295 |

296 |
297 |
298 | ); 299 | }; 300 | 301 | export default AboutPage; 302 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # _We're shifting gears here at Swapi. This Repo will remain available for those that want a copy, but the app itself will include new features and functionality that will not be included in this version._ 2 | 3 | # Documentation 4 | 5 | ### Introduction 6 | 7 | Welcome to the swapi, the Star Wars API! This documentation should help you familiarise yourself with the resources available and how to consume them with HTTP requests. Read through the getting started section before you dive in. Most of your problems should be solved just by reading through it. 8 | 9 | ### Getting started 10 | 11 | Let's make our first API request to the Star Wars API! 12 | 13 | Open up a terminal and use curl or use your browser to make an API request for a resource. In the example below, we're trying to get the first planet, Tatooine: 14 | 15 | https://www.swapi.tech/api/planets/1/ 16 | 17 | Here is the response we get: 18 | 19 | ```json 20 | HTTP/1.0 200 OK 21 | Content-Type: application/json 22 | 23 | { 24 | "climate": "Arid", 25 | "diameter": "10465", 26 | "gravity": "1 standard", 27 | "name": "Tatooine", 28 | "orbital_period": "304", 29 | "population": "200000", 30 | "residents": [ 31 | "https://www.swapi.tech/api/people/1/", 32 | "https://www.swapi.tech/api/people/2/" 33 | ], 34 | "rotation_period": "23", 35 | "surface_water": "1", 36 | "terrain": "Desert", 37 | "url": "https://www.swapi.tech/api/planets/1/", 38 | "etc": "...." 39 | } 40 | ``` 41 | 42 | If your response looks slightly different don't panic. This is probably because more data has been added to swapi since we made this documentation. 43 | 44 | ### Base URL 45 | 46 | The Base URL is the root URL for all of the API, if you ever make a request to swapi and you get back a 404 NOT FOUND response then check the Base URL first. 47 | 48 | The Base URL for swapi is: 49 | 50 | https://www.swapi.tech/api/ 51 | 52 | OR 53 | 54 | https://swapi.tech/api/ 55 | 56 | _The documentation below assumes you are prepending the Base URL to the endpoints in order to make requests._ 57 | 58 | ### Rate limiting 59 | 60 | Swapi has rate limiting to prevent malicious abuse (as if anyone would abuse Star Wars data!) and to make sure our service can handle a potentially large amount of traffic. Rate limiting is done via IP address and is currently limited to 10,000 API request per day. This is enough to request all the data on the website at least ten times over. There should be no reason for hitting the rate limit. 61 | 62 | ### Rate slowing 63 | 64 | Swapi now has rate slowing on top of the rate limiting. Rate slowing is also done via IP address and is currently set to slow by 100ms starting after the 5th API request within a 15 minute window. Each subsequent request will take longer to receieve a response for. 65 | 66 | ### Authentication 67 | 68 | Swapi is a completely open API. No authentication is required to query and get data. This also means that we've limited what you can do to just GET-ing the data. If you find a mistake in the data, email the author at: 69 | 70 | admin@swapi.tech 71 | 72 | ### Searching 73 | 74 | All resources support a search parameter that filters the set of resources returned. This allows you to make queries like: 75 | 76 | > https://www.swapi.tech/api/people/?search=r2 77 | 78 | All searches will use case-insensitive partial matches on the set of search fields. To see the set of search fields for each resource, check out the individual resource documentation. For more information on advanced search terms see here. 79 | 80 | ### Query String Parameters 81 | 82 | Routes that include all resources now supports an 'expanded' parameter that will return all of the requested resource's properties instead of the short hand version: 83 | 84 | > https://www.swapi.tech/api/starships/?expanded=true 85 | 86 | ### Encodings 87 | 88 | SWAPI provides two encodings for you to render the data with: 89 | 90 | 1. JSON 91 | 92 | - JSON is the standard data format provided by SWAPI by default 93 | 94 | 2. Wookiee 95 | - Wookiee is for our tall hairy allies who speak Wookiee, this encoding 96 | returns the same data as json in a stringified syntax, except using with wookiee translations. 97 | 98 | Using the wookiee renderer is easy, just append 99 | 100 | > ?format=wookiee 101 | 102 | to your urls 103 | 104 | EX: 105 | 106 | > https://www.swapi.tech/api/planets/1/?format=wookiee 107 | 108 | # Resources 109 | 110 | ### Root 111 | 112 | The Root resource provides information on all available resources within the API. 113 | 114 | Example request: 115 | 116 | > https://www.swapi.tech/api/ 117 | 118 | Example response: 119 | 120 | ```json 121 | HTTP/1.0 200 OK 122 | Content-Type: application/json 123 | { 124 | "films": "https://www.swapi.tech/api/films/", 125 | "people": "https://www.swapi.tech/api/people/", 126 | "planets": "https://www.swapi.tech/api/planets/", 127 | "species": "https://www.swapi.tech/api/species/", 128 | "starships": "https://www.swapi.tech/api/starships/", 129 | "vehicles": "https://www.swapi.tech/api/vehicles/" 130 | } 131 | ``` 132 | 133 | Attributes: 134 | 135 | ``` 136 | films [string] -- The URL root for Film resources 137 | people [string] -- The URL root for People resources 138 | planets [string] -- The URL root for Planet resources 139 | species [string] -- The URL root for Species resources 140 | starships [string] -- The URL root for Starships resources 141 | vehicles [string] -- The URL root for Vehicles resources 142 | ``` 143 | 144 | ### People 145 | 146 | A People resource is an individual person or character within the Star Wars universe. 147 | 148 | Endpoints: 149 | 150 | ``` 151 | /people/ -- get all the people resources 152 | /people/:id/ -- get a specific people resource 153 | ``` 154 | 155 | Example request: 156 | 157 | > https://www.swapi.tech/api/people/1/ 158 | 159 | Example response: 160 | 161 | ```json 162 | HTTP/1.0 200 OK 163 | Content-Type: application/json 164 | 165 | { 166 | "birth_year": "19 BBY", 167 | "eye_color": "Blue", 168 | "films": [ 169 | "https://www.swapi.tech/api/films/1/" 170 | ], 171 | "gender": "Male", 172 | "hair_color": "Blond", 173 | "height": "172", 174 | "homeworld": "https://www.swapi.tech/api/planets/1/", 175 | "mass": "77", 176 | "name": "Luke Skywalker", 177 | "skin_color": "Fair", 178 | "created": "2014-12-09T13:50:51.644000Z", 179 | "edited": "2014-12-10T13:52:43.172000Z", 180 | "species": [ 181 | "https://www.swapi.tech/api/species/1/" 182 | ], 183 | "starships": [ 184 | "https://www.swapi.tech/api/starships/12/" 185 | ], 186 | "url": "https://www.swapi.tech/api/people/1/", 187 | "vehicles": [ 188 | "https://www.swapi.tech/api/vehicles/14/" 189 | ] 190 | } 191 | ``` 192 | 193 | Attributes: 194 | 195 | ``` 196 | name [string] -- The name of this person. 197 | birth_year [string] -- The birth year of the person, using the in-universe standard of BBY or ABY - Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is a battle that occurs at the end of Star Wars episode IV: A New Hope. 198 | eye_color [string] -- The eye color of this person. Will be "unknown" if not known or "n/a" if the person does not have an eye. 199 | gender [string] -- The gender of this person. Either "Male", "Female" or "unknown", "n/a" if the person does not have a gender. 200 | hair_color [string] -- The hair color of this person. Will be "unknown" if not known or "n/a" if the person does not have hair. 201 | height [string] -- The height of the person in centimeters. 202 | mass [string] -- The mass of the person in kilograms. 203 | skin_color [string] -- The skin color of this person. 204 | homeworld [string] -- The URL of a planet resource, a planet that this person was born on or inhabits. 205 | films [array] -- An array of film resource URLs that this person has been in. 206 | species [array] -- An array of species resource URLs that this person belongs to. 207 | starships [array] -- An array of starship resource URLs that this person has piloted. 208 | vehicles [array] -- An array of vehicle resource URLs that this person has piloted. 209 | url [string] -- the hypermedia URL of this resource. 210 | created [string] -- the ISO 8601 date format of the time that this resource was created. 211 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 212 | ``` 213 | 214 | Search Fields: 215 | 216 | _name_ 217 | 218 | Example: 219 | 220 | > /api/people/1/?name=skywalker 221 | 222 | ### Films 223 | 224 | A Film resource is a single film. 225 | 226 | Endpoints 227 | 228 | ``` 229 | /films/ -- get all the film resources 230 | /films/:id/ -- get a specific film resource 231 | ``` 232 | 233 | Example request: 234 | 235 | > https://www.swapi.tech/api/films/1/ 236 | 237 | Example response: 238 | 239 | ```json 240 | HTTP/1.0 200 OK 241 | Content-Type: application/json 242 | 243 | { 244 | "characters": [ 245 | "https://www.swapi.tech/api/people/1/" 246 | ], 247 | "created": "2014-12-10T14:23:31.880000Z", 248 | "director": "George Lucas", 249 | "edited": "2014-12-12T11:24:39.858000Z", 250 | "episode_id": 4, 251 | "opening_crawl": "It is a period of civil war.\n\nRebel spaceships, striking\n\nfrom a hidden base, have won\n\ntheir first victory against\n\nthe evil Galactic Empire.\n\n\n\nDuring the battle, Rebel\n\nspies managed to steal secret\r\nplans to the Empire's\n\nultimate weapon, the DEATH\n\nSTAR, an armored space\n\nstation with enough power\n\nto destroy an entire planet.\n\n\n\nPursued by the Empire's\n\nsinister agents, Princess\n\nLeia races home aboard her\n\nstarship, custodian of the\n\nstolen plans that can save her\n\npeople and restore\n\nfreedom to the galaxy....", 252 | "planets": [ 253 | "https://www.swapi.tech/api/planets/1/" 254 | ], 255 | "producer": "Gary Kurtz, Rick McCallum", 256 | "release_date": "1977-05-25", 257 | "species": [ 258 | "https://www.swapi.tech/api/species/1/" 259 | ], 260 | "starships": [ 261 | "https://www.swapi.tech/api/starships/2/" 262 | ], 263 | "title": "A New Hope", 264 | "url": "https://www.swapi.tech/api/films/1/", 265 | "vehicles": [ 266 | "https://www.swapi.tech/api/vehicles/4/" 267 | ] 268 | } 269 | ``` 270 | 271 | Attributes: 272 | 273 | ``` 274 | title [string] -- The title of this film 275 | episode_id [integer] -- The episode number of this film. 276 | opening_crawl [string] -- The opening paragraphs at the beginning of this film. 277 | director [string] -- The name of the director of this film. 278 | producer [string] -- The name(s) of the producer(s) of this film. Comma separated. 279 | release_date [date] -- The ISO 8601 date format of film release at original creator country. 280 | species [array] -- An array of species resource URLs that are in this film. 281 | starships [array] -- An array of starship resource URLs that are in this film. 282 | vehicles [array] -- An array of vehicle resource URLs that are in this film. 283 | characters [array] -- An array of people resource URLs that are in this film. 284 | planets [array] -- An array of planet resource URLs that are in this film. 285 | url [string] -- the hypermedia URL of this resource. 286 | created [string] -- the ISO 8601 date format of the time that this resource was created. 287 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 288 | ``` 289 | 290 | Search Fields: 291 | 292 | _title_ 293 | 294 | Example: 295 | 296 | > /api/films/?title=empire 297 | 298 | ### Starships 299 | 300 | A Starship resource is a single transport craft that has hyperdrive capability. 301 | 302 | Endpoints 303 | 304 | ``` 305 | /starships/ -- get all the starship resources 306 | /starships/:id/ -- get a specific starship resource 307 | ``` 308 | 309 | Example request: 310 | 311 | > https://www.swapi.tech/api/starships/9/ 312 | 313 | Example response: 314 | 315 | ```json 316 | HTTP/1.0 200 OK 317 | Content-Type: application/json 318 | 319 | { 320 | "MGLT": "10 MGLT", 321 | "cargo_capacity": "1000000000000", 322 | "consumables": "3 years", 323 | "cost_in_credits": "1000000000000", 324 | "created": "2014-12-10T16:36:50.509000Z", 325 | "crew": "342953", 326 | "edited": "2014-12-10T16:36:50.509000Z", 327 | "hyperdrive_rating": "4.0", 328 | "length": "120000", 329 | "manufacturer": "Imperial Department of Military Research, Sienar Fleet Systems", 330 | "max_atmosphering_speed": "n/a", 331 | "model": "DS-1 Orbital Battle Station", 332 | "name": "Death Star", 333 | "passengers": "843342", 334 | "films": [ 335 | "https://www.swapi.tech/api/films/1/" 336 | ], 337 | "pilots": [], 338 | "starship_class": "Deep Space Mobile Battlestation", 339 | "url": "https://www.swapi.tech/api/starships/9/" 340 | } 341 | ``` 342 | 343 | Attributes: 344 | 345 | ``` 346 | name [string] -- The name of this starship. The common name, such as "Death Star". 347 | model [string] -- The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 Orbital Battle Station". 348 | starship_class [string] -- The class of this starship, such as "Starfighter" or "Deep Space Mobile Battlestation" 349 | manufacturer [string] -- The manufacturer of this starship. Comma separated if more than one. 350 | cost_in_credits [string] -- The cost of this starship new, in galactic credits. 351 | length [string] -- The length of this starship in meters. 352 | crew [string] -- The number of personnel needed to run or pilot this starship. 353 | passengers [string] -- The number of non-essential people this starship can transport. 354 | max_atmosphering_speed [string] -- The maximum speed of this starship in the atmosphere. "N/A" if this starship is incapable of atmospheric flight. 355 | hyperdrive_rating [string] -- The class of this starships hyperdrive. 356 | MGLT [string] -- The Maximum number of Megalights this starship can travel in a standard hour. A "Megalight" is a standard unit of distance and has never been defined before within the Star Wars universe. This figure is only really useful for measuring the difference in speed of starships. We can assume it is similar to AU, the distance between our Sun (Sol) and Earth. 357 | cargo_capacity [string] -- The maximum number of kilograms that this starship can transport. 358 | consumables [string] -- The maximum length of time that this starship can provide consumables for its entire crew without having to resupply. 359 | films [array] -- An array of Film URL Resources that this starship has appeared in. 360 | pilots [array] -- An array of People URL Resources that this starship has been piloted by. 361 | url [string] -- the hypermedia URL of this resource. 362 | created [string] -- the ISO 8601 date format of the time that this resource was created. 363 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 364 | ``` 365 | 366 | Search Fields: 367 | 368 | _name_ 369 | 370 | _model_ 371 | 372 | Example: 373 | 374 | > /api/starships/?name=falcon 375 | 376 | > /api/starships/?model=yt 377 | 378 | ### Vehicles 379 | 380 | A Vehicle resource is a single transport craft that does not have hyperdrive capability. 381 | 382 | Endpoints 383 | 384 | ``` 385 | /vehicles/ -- get all the vehicle resources 386 | /vehicles/:id/ -- get a specific vehicle resource 387 | ``` 388 | 389 | Example request: 390 | 391 | > https://www.swapi.tech/api/vehicles/4/ 392 | 393 | Example response: 394 | 395 | ```json 396 | HTTP/1.0 200 OK 397 | Content-Type: application/json 398 | 399 | { 400 | "cargo_capacity": "50000", 401 | "consumables": "2 months", 402 | "cost_in_credits": "150000", 403 | "created": "2014-12-10T15:36:25.724000Z", 404 | "crew": "46", 405 | "edited": "2014-12-10T15:36:25.724000Z", 406 | "length": "36.8", 407 | "manufacturer": "Corellia Mining Corporation", 408 | "max_atmosphering_speed": "30", 409 | "model": "Digger Crawler", 410 | "name": "Sand Crawler", 411 | "passengers": "30", 412 | "pilots": [], 413 | "films": [ 414 | "https://www.swapi.tech/api/films/1/" 415 | ], 416 | "url": "https://www.swapi.tech/api/vehicles/4/", 417 | "vehicle_class": "wheeled" 418 | } 419 | ``` 420 | 421 | Attributes: 422 | 423 | ``` 424 | name [string] -- The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder bike". 425 | model [string] -- The model or official name of this vehicle. Such as "All-Terrain Attack Transport". 426 | vehicle_class [string] -- The class of this vehicle, such as "Wheeled" or "Repulsorcraft". 427 | manufacturer [string] -- The manufacturer of this vehicle. Comma separated if more than one. 428 | length [string] -- The length of this vehicle in meters. 429 | cost_in_credits [string] -- The cost of this vehicle new, in Galactic Credits. 430 | crew [string] -- The number of personnel needed to run or pilot this vehicle. 431 | passengers [string] -- The number of non-essential people this vehicle can transport. 432 | max_atmosphering_speed [string] -- The maximum speed of this vehicle in the atmosphere. 433 | cargo_capacity [string] -- The maximum number of kilograms that this vehicle can transport. 434 | consumables [string] -- The maximum length of time that this vehicle can provide consumables for its entire crew without having to resupply. 435 | films [array] -- An array of Film URL Resources that this vehicle has appeared in. 436 | pilots [array] -- An array of People URL Resources that this vehicle has been piloted by. 437 | url [string] -- the hypermedia URL of this resource. 438 | created [string] -- the ISO 8601 date format of the time that this resource was created. 439 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 440 | ``` 441 | 442 | Search Fields: 443 | 444 | _name_ 445 | 446 | _model_ 447 | 448 | Example: 449 | 450 | > /api/vehicles/?name=speeder 451 | > /api/vehices/?model=crawler 452 | 453 | ### Species 454 | 455 | A Species resource is a type of person or character within the Star Wars Universe. 456 | 457 | Endpoints 458 | 459 | ``` 460 | /species/ -- get all the species resources 461 | /species/:id/ -- get a specific species resource 462 | ``` 463 | 464 | Example request: 465 | 466 | > https://www.swapi.tech/api/species/3/ 467 | 468 | Example response: 469 | 470 | ```json 471 | HTTP/1.0 200 OK 472 | Content-Type: application/json 473 | 474 | { 475 | "average_height": "2.1", 476 | "average_lifespan": "400", 477 | "classification": "Mammal", 478 | "created": "2014-12-10T16:44:31.486000Z", 479 | "designation": "Sentient", 480 | "edited": "2014-12-10T16:44:31.486000Z", 481 | "eye_colors": "blue, green, yellow, brown, golden, red", 482 | "hair_colors": "black, brown", 483 | "homeworld": "https://www.swapi.tech/api/planets/14/", 484 | "language": "Shyriiwook", 485 | "name": "Wookie", 486 | "people": [ 487 | "https://www.swapi.tech/api/people/13/" 488 | ], 489 | "films": [ 490 | "https://www.swapi.tech/api/films/1/", 491 | "https://www.swapi.tech/api/films/2/" 492 | ], 493 | "skin_colors": "gray", 494 | "url": "https://www.swapi.tech/api/species/3/" 495 | } 496 | ``` 497 | 498 | Attributes: 499 | 500 | ``` 501 | name [string] -- The name of this species. 502 | classification [string] -- The classification of this species, such as "mammal" or "reptile". 503 | designation [string] -- The designation of this species, such as "sentient". 504 | average_height [string] -- The average height of this species in centimeters. 505 | average_lifespan [string] -- The average lifespan of this species in years. 506 | eye_colors [string] -- A comma-separated string of common eye colors for this species, "none" if this species does not typically have eyes. 507 | hair_colors [string] -- A comma-separated string of common hair colors for this species, "none" if this species does not typically have hair. 508 | skin_colors [string] -- A comma-separated string of common skin colors for this species, "none" if this species does not typically have skin. 509 | language [string] -- The language commonly spoken by this species. 510 | homeworld [string] -- The URL of a planet resource, a planet that this species originates from. 511 | people [array] -- An array of People URL Resources that are a part of this species. 512 | films [array] -- An array of Film URL Resources that this species has appeared in. 513 | url [string] -- the hypermedia URL of this resource. 514 | created [string] -- the ISO 8601 date format of the time that this resource was created. 515 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 516 | ``` 517 | 518 | Search Fields: 519 | 520 | _name_ 521 | 522 | Example: 523 | 524 | > /api/species/?name=wookiee 525 | 526 | ### Planets 527 | 528 | A Planet resource is a large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY. 529 | 530 | Endpoints 531 | 532 | ``` 533 | /planets/ -- get all the planets resources 534 | /planets/:id/ -- get a specific planets resource 535 | ``` 536 | 537 | Example request: 538 | 539 | > https://www.swapi.tech/api/planets/1/ 540 | 541 | Example response: 542 | 543 | ```json 544 | HTTP/1.0 200 OK 545 | Content-Type: application/json 546 | 547 | { 548 | "climate": "Arid", 549 | "created": "2014-12-09T13:50:49.641000Z", 550 | "diameter": "10465", 551 | "edited": "2014-12-15T13:48:16.167217Z", 552 | "films": [ 553 | "https://www.swapi.tech/api/films/1/" 554 | ], 555 | "gravity": "1", 556 | "name": "Tatooine", 557 | "orbital_period": "304", 558 | "population": "120000", 559 | "residents": [ 560 | "https://www.swapi.tech/api/people/1/" 561 | ], 562 | "rotation_period": "23", 563 | "surface_water": "1", 564 | "terrain": "Desert", 565 | "url": "https://www.swapi.tech/api/planets/1/" 566 | } 567 | ``` 568 | 569 | Attributes: 570 | 571 | ``` 572 | name [string] -- The name of this planet. 573 | diameter [string] -- The diameter of this planet in kilometers. 574 | rotation_period [string] -- The number of standard hours it takes for this planet to complete a single rotation on its axis. 575 | orbital_period [string] -- The number of standard days it takes for this planet to complete a single orbit of its local star. 576 | gravity [string] -- A number denoting the gravity of this planet, where "1" is normal or 1 standard G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. 577 | population [string] -- The average population of sentient beings inhabiting this planet. 578 | climate [string] -- The climate of this planet. Comma separated if diverse. 579 | terrain [string] -- The terrain of this planet. Comma separated if diverse. 580 | surface_water [string] -- The percentage of the planet surface that is naturally occurring water or bodies of water. 581 | residents [array] -- An array of People URL Resources that live on this planet. 582 | films [array] -- An array of Film URL Resources that this planet has appeared in. 583 | url [string] -- the hypermedia URL of this resource. 584 | created [string] -- the ISO 8601 date format of the time that this resource was created. 585 | edited [string] -- the ISO 8601 date format of the time that this resource was edited. 586 | ``` 587 | 588 | Search Fields: 589 | 590 | _name_ 591 | 592 | Example: 593 | 594 | > /api/planets/?name=tat 595 | 596 | # _Help us Open Source Community... You're our only hope!_ 597 | 598 | - Keep an eye out for our CONTRIBUTIONS.md coming soon, to learn more about contributing to this repository. 599 | - Feel free to create a pull request on any other issues you find 600 | - Don't hesitate to help with styling these docs! 601 | 602 | Enjoy! 603 | -------------------------------------------------------------------------------- /client/src/pages/DocsPage.jsx: -------------------------------------------------------------------------------- 1 | const DocsPage = () => { 2 | return ( 3 |
4 |
5 |
Getting started
6 | 35 | 36 |
Encodings
37 | 45 |
Resources
46 | 69 |
Helper libraries
70 |
    71 |
  • 72 | React 73 |
  • 74 | 75 |
  • 76 | Ruby 77 |
  • 78 |
79 |
80 | 81 |
82 |

Documentation

83 |
84 | 85 |

Introduction

86 |

87 | Welcome to the swapi, the Star Wars API! This documentation should 88 | help you familiarise yourself with the resources available and how to 89 | consume them with HTTP requests. If you're after a native helper 90 | library then I suggest you scroll down and check out what's available. 91 | Read through the getting started section before you dive in. Most of 92 | your problems should be solved just by reading through it. 93 |

94 | 95 |

Getting started

96 |

Let's make our first API request to the Star Wars API!

97 | 98 |

99 | Open up a terminal and use{" "} 100 | 105 | curl 106 | {" "} 107 | or use a{" "} 108 | 113 | fetch 114 | {" "} 115 | call to make an API request for a resource. In the example below, 116 | we're trying to get the first planet, Tatooine: 117 |

118 | 119 |
 120 | 					
 121 | 						{`
 122 | fetch("https://www.swapi.tech/api/planets/1/")
 123 | .then(res => res.json())
 124 | .then(data => console.log(data))
 125 | .catch(err => console.error(err))
 126 |               `}
 127 | 					
 128 | 				
129 | 130 |

131 | We'll use{" "} 132 | 137 | fetch 138 | {" "} 139 | If you don't want to use the fetch api, just use the curl{" "} 140 | command, your browser window, or{" "} 141 | 146 | Postman 147 | {" "} 148 | instead. 149 |

150 | 151 |

Here is the response we get:

152 | 153 |
 154 | 					
 155 | 						{`{
 156 |     message: "ok",
 157 |     result: {
 158 |       properties: {
 159 |         climate: "Arid",
 160 |         diameter: "10465",
 161 |         gravity: "1 standard",
 162 |         name: "Tatooine",
 163 |         orbital_period: "304",
 164 |         population: "200000",
 165 |         residents: [
 166 |           "https://www.swapi.tech/api/people/1/",
 167 |           "https://www.swapi.tech/api/people/2/",
 168 |           ...
 169 |         ],
 170 |         rotation_period: "23",
 171 |         surface_water: "1",
 172 |         terrain: "Desert",
 173 |         url: "https://www.swapi.tech/api/planets/1/",
 174 |       }
 175 |     }
 176 |     ...
 177 | }`}
 178 | 					
 179 | 				
180 | 181 |

182 | If your response looks slightly different don't panic. This is 183 | probably because more data has been added to swapi since we made this 184 | documentation. 185 |

186 | 187 |

Base URL

188 |

189 | The Base URL is the root URL for all of the API, if 190 | you ever make a request to swapi and you get back a{" "} 191 | 404 NOT FOUND response then check the Base URL first. 192 |

193 | 194 |

The Base URL for swapi is:

195 | 196 |
 197 | 					https://www.swapi.tech/api/
 198 | 				
199 | 200 |

OR

201 | 202 |
 203 | 					https://swapi.tech/api/
 204 | 				
205 | 206 |

207 | The documentation below assumes you are prepending the Base URL to the 208 | endpoints in order to make requests. 209 |

210 | 211 |

Rate limiting

212 | 213 |

214 | Swapi has rate limiting to prevent malicious abuse (as if anyone would 215 | abuse Star Wars data!) and to make sure our service can handle a 216 | potentially large amount of traffic. Rate limiting is done via IP 217 | address and is currently limited to 10,000 API request per day. This 218 | is enough to request all the data on the website at least ten times 219 | over. There should be no reason for hitting the rate limit. 220 |

221 | 222 |

Rate slowing

223 | 224 |

225 | Swapi now has rate slowing on top of the rate limiting. Rate slowing 226 | is also done via IP address and is currently set to slow by 100ms 227 | starting after the 5th API request within a 15 minute window. Each 228 | subsequent request will take longer to receieve a response for. 229 |

230 | 231 |

Authentication

232 | 233 |

234 | Swapi is a completely open API. No authentication is 235 | required to query and get data. This also means that we've limited 236 | what you can do to just GET-ing the data. If you find 237 | a mistake in the data, then{" "} 238 | console.log("author email: admin@swapi.tech")} 240 | href="mailto:admin@swapi.tech" 241 | target="_blank" 242 | rel="noopener noreferrer" 243 | > 244 | email the author 245 | 246 | . 247 |

248 | 249 |

JSON Schema

250 |

Coming Soon

251 | {/*

252 | All resources support JSON Schema 253 | . Making a request to /api/<resource>/schema will 254 | give you the details of that resource. This will allow you to 255 | programmatically inspect the attributes of that resource and their 256 | types. 257 |

*/} 258 | 259 | 260 | 261 |

262 | All resources support a search parameter that filters the 263 | set of resources returned. This allows you to make queries like: 264 |

265 | 266 |

267 | https://www.swapi.tech/api/people/?name=r2 268 |

269 | 270 |

271 | All searches will use case-insensitive partial matches on the set of 272 | search fields. To see the set of search fields for each resource, 273 | check out the individual resource documentation. 274 |

275 | 276 |

Query String Parameters

277 | 278 |

279 | Routes that include all resources now supports an{" "} 280 | expanded parameter that will return all of the requested 281 | resource's properties instead of the short hand version: 282 |

283 | 284 |

285 | https://www.swapi.tech/api/starships/?expanded=true 286 |

287 | 288 |

Encodings

289 | 290 |
291 | 292 |

SWAPI provides two encodings for you to render the data with:

293 | 294 |

JSON

295 | 296 |

JSON is the standard data format provided by SWAPI by default.

297 | 298 |

Wookiee

299 | 300 |

301 | Wookiee is for our tall hairy allies who speak Wookiee, this encoding 302 | returns the same data as json in a stringified syntax, except using 303 | with wookiee translations. 304 |

305 | 306 |

307 | Using the wookiee renderer is easy, just append{" "} 308 | ?format=wookiee to your urls: 309 |

310 | 311 |

312 | https://www.swapi.tech/api/planets/1/?format=wookiee 313 |

314 | 315 |

Resources

316 | 317 |
318 | 319 |

Root

320 | 321 |

322 | The Root resource provides information on all available resources 323 | within the API. 324 |

325 | 326 |

327 | Example request: 328 |

329 | 330 |
 331 | 					
 332 | 						{" "}
 333 | 						{`
 334 | fetch("https://www.swapi.tech/api")
 335 | .then(res => res.json())
 336 | .then(data => console.log(data))
 337 | .catch(err => console.error(err))
 338 |               `}
 339 | 					
 340 | 				
341 | 342 |

343 | Example response: 344 |

345 | 346 |
 347 | 					
 348 | 						{`{
 349 |     "films": "https://www.swapi.tech/api/films/", 
 350 |     "people": "https://www.swapi.tech/api/people/", 
 351 |     "planets": "https://www.swapi.tech/api/planets/", 
 352 |     "species": "https://www.swapi.tech/api/species/", 
 353 |     "starships": "https://www.swapi.tech/api/starships/", 
 354 |     "vehicles": "https://www.swapi.tech/api/vehicles/"
 355 |   }`}
 356 | 					
 357 | 				
358 | 359 |

360 | Attributes: 361 |

362 | 363 |
    364 |
  • 365 | films string 366 | -- The URL root for Film resources 367 |
  • 368 |
  • 369 | people string 370 | -- The URL root for People resources 371 |
  • 372 |
  • 373 | planets string 374 | -- The URL root for Planet resources 375 |
  • 376 |
  • 377 | species string 378 | -- The URL root for Species resources 379 |
  • 380 |
  • 381 | starships string 382 | -- The URL root for Starships resources 383 |
  • 384 |
  • 385 | vehicles string 386 | -- The URL root for Vehicles resources 387 |
  • 388 |
389 | 390 |
391 | 392 |

People

393 | 394 |

395 | A People resource is an individual person or character within the Star 396 | Wars universe. 397 |

398 | 399 |

400 | Endpoints 401 |

402 | 403 |
    404 |
  • 405 | /people/ -- get all the people resources 406 |
  • 407 |
  • 408 | /people/:id/ -- get a specific people resource 409 |
  • 410 | {/*
  • 411 | /people/schema/ -- view the JSON schema for this 412 | resource 413 |
  • */} 414 |
415 | 416 |

417 | Example request: 418 |

419 | 420 |
 421 | 					
 422 | 						{" "}
 423 | 						{`
 424 | fetch("https://www.swapi.tech/api/people/1")
 425 | .then(res => res.json())
 426 | .then(data => console.log(data))
 427 | .catch(err => console.error(err))
 428 |               `}
 429 | 					
 430 | 				
431 | 432 |

433 | Example response: 434 |

435 | 436 |
 437 | 					
 438 | 						{`{
 439 |   ...
 440 |   properties: {
 441 |     "birth_year": "19 BBY", 
 442 |     "eye_color": "Blue", 
 443 |     "films": [ "https://www.swapi.tech/api/films/1/", ... ], 
 444 |     "gender": "Male", 
 445 |     "hair_color": "Blond", 
 446 |     "height": "172", 
 447 |     "homeworld": "https://www.swapi.tech/api/planets/1/", 
 448 |     "mass": "77", 
 449 |     "name": "Luke Skywalker", 
 450 |     "skin_color": "Fair", 
 451 |     "created": "2014-12-09T13:50:51.644000Z", 
 452 |     "edited": "2014-12-10T13:52:43.172000Z", 
 453 |     "species": [ "https://www.swapi.tech/api/species/1/" ], 
 454 |     "starships": [ "https://www.swapi.tech/api/starships/12/", ... ], 
 455 |     "url": "https://www.swapi.tech/api/people/1/", 
 456 |     "vehicles": [ "https://www.swapi.tech/api/vehicles/14/" ... ]
 457 |   }
 458 | }`}
 459 | 					
 460 | 				
461 | 462 |

463 | Attributes: 464 |

465 | 466 |
    467 |
  • 468 | name string 469 | -- The name of this person. 470 |
  • 471 |
  • 472 | birth_year string 473 | -- The birth year of the person, using the in-universe standard of{" "} 474 | BBY or ABY - Before the Battle of 475 | Yavin or After the Battle of Yavin. The Battle of Yavin is a battle 476 | that occurs at the end of Star Wars episode IV: A New Hope. 477 |
  • 478 |
  • 479 | eye_color string 480 | -- The eye color of this person. Will be "unknown" if not known or 481 | "n/a" if the person does not have an eye. 482 |
  • 483 |
  • 484 | gender string 485 | -- The gender of this person. Either "Male", "Female" or "unknown", 486 | "n/a" if the person does not have a gender. 487 |
  • 488 |
  • 489 | hair_color string 490 | -- The hair color of this person. Will be "unknown" if not known or 491 | "n/a" if the person does not have hair. 492 |
  • 493 |
  • 494 | height string 495 | -- The height of the person in centimeters. 496 |
  • 497 |
  • 498 | mass string 499 | -- The mass of the person in kilograms. 500 |
  • 501 |
  • 502 | skin_color string 503 | -- The skin color of this person. 504 |
  • 505 |
  • 506 | homeworld string 507 | -- The URL of a planet resource, a planet that this person was born 508 | on or inhabits. 509 |
  • 510 |
  • 511 | films array 512 | -- An array of film resource URLs that this person has been in. 513 |
  • 514 |
  • 515 | species array 516 | -- An array of species resource URLs that this person belongs to. 517 |
  • 518 |
  • 519 | starships array 520 | -- An array of starship resource URLs that this person has piloted. 521 |
  • 522 |
  • 523 | vehicles array 524 | -- An array of vehicle resource URLs that this person has piloted. 525 |
  • 526 |
  • 527 | url string 528 | -- the hypermedia URL of this resource. 529 |
  • 530 |
  • 531 | created string 532 | -- the ISO 8601 date format of the time that this resource was 533 | created. 534 |
  • 535 |
  • 536 | edited string 537 | -- the ISO 8601 date format of the time that this resource was 538 | edited. 539 |
  • 540 |
541 | 542 |

543 | Search Fields: 544 |

545 | 546 |
    547 |
  • 548 | name 549 |
  • 550 |
551 | 552 |
553 | 554 |

Films

555 | 556 |

A Film resource is a single film.

557 | 558 |

559 | Endpoints 560 |

561 | 562 |
    563 |
  • 564 | /films/ -- get all the film resources 565 |
  • 566 |
  • 567 | /films/:id/ -- get a specific film resource 568 |
  • 569 | {/*
  • 570 | /films/schema/ -- view the JSON schema for this 571 | resource 572 |
  • */} 573 |
574 | 575 |

576 | Example request: 577 |

578 | 579 |
 580 | 					
 581 | 						{" "}
 582 | 						{`
 583 | fetch("https://www.swapi.tech/api/films/1")
 584 | .then(res => res.json())
 585 | .then(data => console.log(data))
 586 | .catch(err => console.error(err))
 587 |               `}
 588 | 					
 589 | 				
590 | 591 |

592 | Example response: 593 |

594 | 595 |
 596 | 					
 597 | 						{`{
 598 |     ...
 599 |     "properties": {
 600 |       "characters": [ "https://www.swapi.tech/api/people/1/", ... ],
 601 |       "created": "2014-12-10T14:23:31.880000Z",
 602 |       "director": "George Lucas",
 603 |       "edited": "2014-12-12T11:24:39.858000Z",
 604 |       "episode_id": 4,
 605 |       "opening_crawl": "It is a period of civil war ...",
 606 |       "planets": [ "https://www.swapi.tech/api/planets/1/", ... ],
 607 |       "producer": "Gary Kurtz, Rick McCallum",
 608 |       "release_date": "1977-05-25",
 609 |       "species": [ "https://www.swapi.tech/api/species/1/", ... ],
 610 |       "starships": [ "https://www.swapi.tech/api/starships/2/", ... ],
 611 |       "title": "A New Hope",
 612 |       "url": "https://www.swapi.tech/api/films/1/",
 613 |       "vehicles": [ "https://www.swapi.tech/api/vehicles/4/", ... ]
 614 |     }
 615 |   }`}
 616 | 					
 617 | 				
618 | 619 |

620 | Attributes: 621 |

622 | 623 |
    624 |
  • 625 | title string 626 | -- The title of this film 627 |
  • 628 |
  • 629 | episode_id integer 630 | -- The episode number of this film. 631 |
  • 632 |
  • 633 | opening_crawl string 634 | -- The opening paragraphs at the beginning of this film. 635 |
  • 636 |
  • 637 | director string 638 | -- The name of the director of this film. 639 |
  • 640 |
  • 641 | producer string 642 | -- The name(s) of the producer(s) of this film. Comma separated. 643 |
  • 644 |
  • 645 | release_date date 646 | -- The ISO 8601 date format of film release at original creator 647 | country. 648 |
  • 649 |
  • 650 | species array 651 | -- An array of species resource URLs that are in this film. 652 |
  • 653 |
  • 654 | starships array 655 | -- An array of starship resource URLs that are in this film. 656 |
  • 657 |
  • 658 | vehicles array 659 | -- An array of vehicle resource URLs that are in this film. 660 |
  • 661 |
  • 662 | characters array 663 | -- An array of people resource URLs that are in this film. 664 |
  • 665 |
  • 666 | planets array 667 | -- An array of planet resource URLs that are in this film. 668 |
  • 669 |
  • 670 | url string 671 | -- the hypermedia URL of this resource. 672 |
  • 673 |
  • 674 | created string 675 | -- the ISO 8601 date format of the time that this resource was 676 | created. 677 |
  • 678 |
  • 679 | edited string 680 | -- the ISO 8601 date format of the time that this resource was 681 | edited. 682 |
  • 683 |
684 | 685 |

686 | Search Fields: 687 |

688 | 689 |
    690 |
  • 691 | title 692 |
  • 693 |
694 | 695 |
696 | 697 |

Starships

698 | 699 |

700 | A Starship resource is a single transport craft that has hyperdrive 701 | capability. 702 |

703 | 704 |

705 | Endpoints 706 |

707 | 708 |
    709 |
  • 710 | /starships/ -- get all the starship resources 711 |
  • 712 |
  • 713 | /starships/:id/ -- get a specific starship resource 714 |
  • 715 | {/*
  • 716 | /starships/schema/ -- view the JSON schema for this 717 | resource 718 |
  • */} 719 |
720 | 721 |

722 | Example request: 723 |

724 | 725 |
 726 | 					
 727 | 						{" "}
 728 | 						{`
 729 | fetch("https://www.swapi.tech/api/starships/9")
 730 | .then(res => res.json())
 731 | .then(data => console.log(data))
 732 | .catch(err => console.error(err))
 733 |               `}
 734 | 					
 735 | 				
736 | 737 |

738 | Example response: 739 |

740 | 741 |
 742 | 					
 743 | 						{`{
 744 |   ...
 745 |   "properties": {
 746 |     "MGLT": "10 MGLT",
 747 |     "cargo_capacity": "1000000000000",
 748 |     "consumables": "3 years",
 749 |     "cost_in_credits": "1000000000000",
 750 |     "created": "2014-12-10T16:36:50.509000Z",
 751 |     "crew": "342953",
 752 |     "edited": "2014-12-10T16:36:50.509000Z",
 753 |     "hyperdrive_rating": "4.0",
 754 |     "length": "120000",
 755 |     "manufacturer": "Imperial Department of Military Research, Sienar Fleet Systems",
 756 |     "max_atmosphering_speed": "n/a",
 757 |     "model": "DS-1 Orbital Battle Station",
 758 |     "name": "Death Star",
 759 |     "passengers": "843342",
 760 |     "films": [ "https://www.swapi.tech/api/films/1/" ],
 761 |     "pilots": [],
 762 |     "starship_class": "Deep Space Mobile Battlestation",
 763 |     "url": "https://www.swapi.tech/api/starships/9/"
 764 |   }
 765 | }`}
 766 | 					
 767 | 				
768 | 769 |

770 | Attributes: 771 |

772 | 773 |
    774 |
  • 775 | name string 776 | -- The name of this starship. The common name, such as "Death Star". 777 |
  • 778 |
  • 779 | model string 780 | -- The model or official name of this starship. Such as "T-65 781 | X-wing" or "DS-1 Orbital Battle Station". 782 |
  • 783 |
  • 784 | starship_class string 785 | -- The class of this starship, such as "Starfighter" or "Deep Space 786 | Mobile Battlestation" 787 |
  • 788 |
  • 789 | manufacturer string 790 | -- The manufacturer of this starship. Comma separated if more than 791 | one. 792 |
  • 793 |
  • 794 | cost_in_credits string 795 | -- The cost of this starship new, in galactic credits. 796 |
  • 797 |
  • 798 | length string 799 | -- The length of this starship in meters. 800 |
  • 801 |
  • 802 | crew string 803 | -- The number of personnel needed to run or pilot this starship. 804 |
  • 805 |
  • 806 | passengers string 807 | -- The number of non-essential people this starship can transport. 808 |
  • 809 |
  • 810 | max_atmosphering_speed string 811 | -- The maximum speed of this starship in the atmosphere. "N/A" if 812 | this starship is incapable of atmospheric flight. 813 |
  • 814 |
  • 815 | hyperdrive_rating string 816 | -- The class of this starships hyperdrive. 817 |
  • 818 |
  • 819 | MGLT string 820 | -- The Maximum number of Megalights this starship can travel in a 821 | standard hour. A "Megalight" is a standard unit of distance and has 822 | never been defined before within the Star Wars universe. This figure 823 | is only really useful for measuring the difference in speed of 824 | starships. We can assume it is similar to AU, the distance between 825 | our Sun (Sol) and Earth. 826 |
  • 827 |
  • 828 | cargo_capacity string 829 | -- The maximum number of kilograms that this starship can transport. 830 |
  • 831 |
  • 832 | consumables *string 833 |
  • 834 |
  • 835 | The maximum length of time that this starship can provide 836 | consumables for its entire crew without having to resupply. 837 |
  • 838 |
  • 839 | films array 840 | -- An array of Film URL Resources that this starship has appeared 841 | in. 842 |
  • 843 |
  • 844 | pilots array 845 | -- An array of People URL Resources that this starship has been 846 | piloted by. 847 |
  • 848 |
  • 849 | url string 850 | -- the hypermedia URL of this resource. 851 |
  • 852 |
  • 853 | created string 854 | -- the ISO 8601 date format of the time that this resource was 855 | created. 856 |
  • 857 |
  • 858 | edited string 859 | -- the ISO 8601 date format of the time that this resource was 860 | edited. 861 |
  • 862 |
863 | 864 |

865 | Search Fields: 866 |

867 | 868 |
    869 |
  • 870 | name 871 |
  • 872 |
  • 873 | model 874 |
  • 875 |
876 | 877 |
878 | 879 |

Vehicles

880 | 881 |

882 | A Vehicle resource is a single transport craft that{" "} 883 | does not have hyperdrive capability. 884 |

885 | 886 |

887 | Endpoints 888 |

889 | 890 |
    891 |
  • 892 | /vehicles/ -- get all the vehicle resources 893 |
  • 894 |
  • 895 | /vehicles/:id/ -- get a specific vehicle resource 896 |
  • 897 | {/*
  • 898 | /vehicles/schema/ -- view the JSON schema for this 899 | resource 900 |
  • */} 901 |
902 | 903 |

904 | Example request: 905 |

906 | 907 |
 908 | 					
 909 | 						{" "}
 910 | 						{`
 911 | fetch("https://www.swapi.tech/api/vehicles/4")
 912 | .then(res => res.json())
 913 | .then(data => console.log(data))
 914 | .catch(err => console.error(err))
 915 |               `}
 916 | 					
 917 | 				
918 | 919 |

920 | Example response: 921 |

922 | 923 |
 924 | 					
 925 | 						{`{
 926 |   ...
 927 |   "properties": {
 928 |     "cargo_capacity": "50000",
 929 |     "consumables": "2 months",
 930 |     "cost_in_credits": "150000",
 931 |     "created": "2014-12-10T15:36:25.724000Z",
 932 |     "crew": "46",
 933 |     "edited": "2014-12-10T15:36:25.724000Z",
 934 |     "length": "36.8",
 935 |     "manufacturer": "Corellia Mining Corporation",
 936 |     "max_atmosphering_speed": "30",
 937 |     "model": "Digger Crawler",
 938 |     "name": "Sand Crawler",
 939 |     "passengers": "30",
 940 |     "pilots": [],
 941 |     "films": [ "https://www.swapi.tech/api/films/1/" ],
 942 |     "url": "https://www.swapi.tech/api/vehicles/4/",
 943 |     "vehicle_class": "wheeled"
 944 |   }
 945 | }`}
 946 | 					
 947 | 				
948 | 949 |

950 | Attributes: 951 |

952 | 953 |
    954 |
  • 955 | name string 956 | -- The name of this vehicle. The common name, such as "Sand Crawler" 957 | or "Speeder bike". 958 |
  • 959 |
  • 960 | model string 961 | -- The model or official name of this vehicle. Such as "All-Terrain 962 | Attack Transport". 963 |
  • 964 |
  • 965 | vehicle_class string 966 | -- The class of this vehicle, such as "Wheeled" or "Repulsorcraft". 967 |
  • 968 |
  • 969 | manufacturer string 970 | -- The manufacturer of this vehicle. Comma separated if more than 971 | one. 972 |
  • 973 |
  • 974 | length string 975 | -- The length of this vehicle in meters. 976 |
  • 977 |
  • 978 | cost_in_credits string 979 | -- The cost of this vehicle new, in Galactic Credits. 980 |
  • 981 |
  • 982 | crew string 983 | -- The number of personnel needed to run or pilot this vehicle. 984 |
  • 985 |
  • 986 | passengers string 987 | -- The number of non-essential people this vehicle can transport. 988 |
  • 989 |
  • 990 | max_atmosphering_speed string 991 | -- The maximum speed of this vehicle in the atmosphere. 992 |
  • 993 |
  • 994 | cargo_capacity string 995 | -- The maximum number of kilograms that this vehicle can transport. 996 |
  • 997 |
  • 998 | consumables *string 999 |
  • 1000 |
  • 1001 | The maximum length of time that this vehicle can provide consumables 1002 | for its entire crew without having to resupply. 1003 |
  • 1004 |
  • 1005 | films array 1006 | -- An array of Film URL Resources that this vehicle has appeared in. 1007 |
  • 1008 |
  • 1009 | pilots array 1010 | -- An array of People URL Resources that this vehicle has been 1011 | piloted by. 1012 |
  • 1013 |
  • 1014 | url string 1015 | -- the hypermedia URL of this resource. 1016 |
  • 1017 |
  • 1018 | created string 1019 | -- the ISO 8601 date format of the time that this resource was 1020 | created. 1021 |
  • 1022 |
  • 1023 | edited string 1024 | -- the ISO 8601 date format of the time that this resource was 1025 | edited. 1026 |
  • 1027 |
1028 | 1029 |

1030 | Search Fields: 1031 |

1032 | 1033 |
    1034 |
  • 1035 | name 1036 |
  • 1037 |
  • 1038 | model 1039 |
  • 1040 |
1041 | 1042 |
1043 | 1044 |

Species

1045 | 1046 |

1047 | A Species resource is a type of person or character within the Star 1048 | Wars Universe. 1049 |

1050 | 1051 |

1052 | Endpoints 1053 |

1054 | 1055 |
    1056 |
  • 1057 | /species/ -- get all the species resources 1058 |
  • 1059 |
  • 1060 | /species/:id/ -- get a specific species resource 1061 |
  • 1062 | {/*
  • 1063 | /species/schema/ -- view the JSON schema for this 1064 | resource 1065 |
  • */} 1066 |
1067 | 1068 |

1069 | Example request: 1070 |

1071 | 1072 |
1073 | 					
1074 | 						{" "}
1075 | 						{`
1076 | fetch("https://www.swapi.tech/api/species/3")
1077 | .then(res => res.json())
1078 | .then(data => console.log(data))
1079 | .catch(err => console.error(err))
1080 |               `}
1081 | 					
1082 | 				
1083 | 1084 |

1085 | Example response: 1086 |

1087 | 1088 |
1089 | 					
1090 | 						{`{
1091 |   ...
1092 |   "properties": {
1093 |     "average_height": "2.1",
1094 |     "average_lifespan": "400",
1095 |     "classification": "Mammal",
1096 |     "created": "2014-12-10T16:44:31.486000Z",
1097 |     "designation": "Sentient",
1098 |     "edited": "2014-12-10T16:44:31.486000Z",
1099 |     "eye_colors": "blue, green, yellow, brown, golden, red",
1100 |     "hair_colors": "black, brown",
1101 |     "homeworld": "https://www.swapi.tech/api/planets/14/",
1102 |     "language": "Shyriiwook",
1103 |     "name": "Wookie",
1104 |     "people": [ "https://www.swapi.tech/api/people/13/" ],
1105 |     "films": [ "https://www.swapi.tech/api/films/1/", ... ],
1106 |     "skin_colors": "gray",
1107 |     "url": "https://www.swapi.tech/api/species/3/"
1108 |   }
1109 | }`}
1110 | 					
1111 | 				
1112 | 1113 |

1114 | Attributes: 1115 |

1116 | 1117 |
    1118 |
  • 1119 | name string 1120 | -- The name of this species. 1121 |
  • 1122 |
  • 1123 | classification string 1124 | -- The classification of this species, such as "mammal" or 1125 | "reptile". 1126 |
  • 1127 |
  • 1128 | designation string 1129 | -- The designation of this species, such as "sentient". 1130 |
  • 1131 |
  • 1132 | average_height string 1133 | -- The average height of this species in centimeters. 1134 |
  • 1135 |
  • 1136 | average_lifespan string 1137 | -- The average lifespan of this species in years. 1138 |
  • 1139 |
  • 1140 | eye_colors string 1141 | -- A comma-separated string of common eye colors for this species, 1142 | "none" if this species does not typically have eyes. 1143 |
  • 1144 |
  • 1145 | hair_colors string 1146 | -- A comma-separated string of common hair colors for this species, 1147 | "none" if this species does not typically have hair. 1148 |
  • 1149 |
  • 1150 | skin_colors string 1151 | -- A comma-separated string of common skin colors for this species, 1152 | "none" if this species does not typically have skin. 1153 |
  • 1154 |
  • 1155 | language string 1156 | -- The language commonly spoken by this species. 1157 |
  • 1158 |
  • 1159 | homeworld string 1160 | -- The URL of a planet resource, a planet that this species 1161 | originates from. 1162 |
  • 1163 |
  • 1164 | people array 1165 | -- An array of People URL Resources that are a part of this species. 1166 |
  • 1167 |
  • 1168 | films array 1169 | -- An array of Film URL Resources that this species has appeared in. 1170 |
  • 1171 |
  • 1172 | url string 1173 | -- the hypermedia URL of this resource. 1174 |
  • 1175 |
  • 1176 | created string 1177 | -- the ISO 8601 date format of the time that this resource was 1178 | created. 1179 |
  • 1180 |
  • 1181 | edited string 1182 | -- the ISO 8601 date format of the time that this resource was 1183 | edited. 1184 |
  • 1185 |
1186 | 1187 |

1188 | Search Fields: 1189 |

1190 | 1191 |
    1192 |
  • 1193 | name 1194 |
  • 1195 |
1196 | 1197 |
1198 | 1199 |

Planets

1200 | 1201 |

1202 | A Planet resource is a large mass, planet or planetoid in the Star 1203 | Wars Universe, at the time of 0 ABY. 1204 |

1205 | 1206 |

1207 | Endpoints 1208 |

1209 | 1210 |
    1211 |
  • 1212 | /planets/ -- get all the planets resources 1213 |
  • 1214 |
  • 1215 | /planets/:id/ -- get a specific planets resource 1216 |
  • 1217 | {/*
  • 1218 | /planets/schema/ -- view the JSON schema for this 1219 | resource 1220 |
  • */} 1221 |
1222 | 1223 |

1224 | Example request: 1225 |

1226 | 1227 |
1228 | 					
1229 | 						{" "}
1230 | 						{`
1231 | fetch("https://www.swapi.tech/api/planets/1")
1232 | .then(res => res.json())
1233 | .then(data => console.log(data))
1234 | .catch(err => console.error(err))
1235 |               `}
1236 | 					
1237 | 				
1238 | 1239 |

1240 | Example response: 1241 |

1242 | 1243 |
1244 | 					
1245 | 						{`{
1246 |   ...
1247 |   "properties": {
1248 |     "climate": "Arid",
1249 |     "created": "2014-12-09T13:50:49.641000Z",
1250 |     "diameter": "10465",
1251 |     "edited": "2014-12-15T13:48:16.167217Z",
1252 |     "films": [ "https://www.swapi.tech/api/films/1/", ... ],
1253 |     "gravity": "1",
1254 |     "name": "Tatooine",
1255 |     "orbital_period": "304",
1256 |     "population": "120000",
1257 |     "residents": [ "https://www.swapi.tech/api/people/1/", ... ],
1258 |     "rotation_period": "23",
1259 |     "surface_water": "1",
1260 |     "terrain": "Dessert",
1261 |     "url": "https://www.swapi.tech/api/planets/1/"
1262 |   }
1263 | }`}
1264 | 					
1265 | 				
1266 | 1267 |

1268 | Attributes: 1269 |

1270 | 1271 |
    1272 |
  • 1273 | name string 1274 | -- The name of this planet. 1275 |
  • 1276 |
  • 1277 | diameter string 1278 | -- The diameter of this planet in kilometers. 1279 |
  • 1280 |
  • 1281 | rotation_period string 1282 | -- The number of standard hours it takes for this planet to complete 1283 | a single rotation on its axis. 1284 |
  • 1285 |
  • 1286 | orbital_period string 1287 | -- The number of standard days it takes for this planet to complete 1288 | a single orbit of its local star. 1289 |
  • 1290 |
  • 1291 | gravity string 1292 | -- A number denoting the gravity of this planet, where "1" is normal 1293 | or 1 standard G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 1294 | standard Gs. 1295 |
  • 1296 |
  • 1297 | population string 1298 | -- The average population of sentient beings inhabiting this planet. 1299 |
  • 1300 |
  • 1301 | climate string 1302 | -- The climate of this planet. Comma separated if diverse. 1303 |
  • 1304 |
  • 1305 | terrain string 1306 | -- The terrain of this planet. Comma separated if diverse. 1307 |
  • 1308 |
  • 1309 | surface_water string 1310 | -- The percentage of the planet surface that is naturally occurring 1311 | water or bodies of water. 1312 |
  • 1313 |
  • 1314 | residents array 1315 | -- An array of People URL Resources that live on this planet. 1316 |
  • 1317 |
  • 1318 | films array 1319 | -- An array of Film URL Resources that this planet has appeared in. 1320 |
  • 1321 |
  • 1322 | url string 1323 | -- the hypermedia URL of this resource. 1324 |
  • 1325 |
  • 1326 | created string 1327 | -- the ISO 8601 date format of the time that this resource was 1328 | created. 1329 |
  • 1330 |
  • 1331 | edited string 1332 | -- the ISO 8601 date format of the time that this resource was 1333 | edited. 1334 |
  • 1335 |
1336 | 1337 |

1338 | Search Fields: 1339 |

1340 | 1341 |
    1342 |
  • 1343 | name 1344 |
  • 1345 |
1346 | 1347 |

Helper libraries

1348 | 1349 |
1350 | 1351 |

1352 | There are helper libraries available for consuming the Star Wars API 1353 | in a native programming language. Be on the look out for more, or 1354 | submit your own: support@swapi.tech 1355 |

1356 | 1357 |
1358 |

React

1359 | 1380 | 1381 |

Ruby

1382 | 1402 |
1403 |
1404 |
1405 | ); 1406 | }; 1407 | 1408 | export default DocsPage; 1409 | --------------------------------------------------------------------------------