├── .prettierrc ├── public ├── .nojekyll └── images │ ├── author.jpg │ ├── favicon.ico │ ├── course-icon.png │ ├── BRAND-WHearts.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ └── social-share-cover.jpg ├── lessons ├── 07-auth │ ├── meta.json │ ├── B-users.md │ └── A-auth-middleware.md ├── 12-testing │ ├── meta.json │ ├── A-setting-up-test.md │ └── B-types-of-tests.md ├── 01-start-here │ ├── meta.json │ ├── A-intro.md │ ├── C-what-we-are-building.md │ └── B-tools.md ├── 04-intro-to-prisma │ ├── meta.json │ ├── B-db-setup.md │ └── A-what-is-prisma.md ├── 05-data-modeling │ ├── meta.json │ ├── A-design-a-schema.md │ ├── C-first-migrations.md │ └── B-creating-models.md ├── 09-error-handlers │ ├── meta.json │ ├── B-custom-handler.md │ └── A-error-handler.md ├── 11-perf │ ├── meta.json │ └── A-async-everything.md ├── 08-route-handlers │ ├── meta.json │ ├── B-handlers.md │ └── A-validating-inputs.md ├── 10-global-config │ ├── meta.json │ ├── B-configs.md │ └── A-env-and-stage.md ├── 06-routes-and-middleware │ ├── meta.json │ ├── C-errata-nodemon.md │ ├── B-middleware.md │ └── A-creating-routes.md ├── 02-api-basics-in-node │ ├── meta.json │ ├── A-vanilla-server.md │ └── B-api-anatomy.md ├── 03-intro-to-express │ ├── meta.json │ ├── A-express-enters-the-chat.md │ └── B-easy-mode-with-express.md └── 13-deployment-and-next-steps │ ├── B-next-steps.md │ └── A-deploying.md ├── context ├── headerContext.js └── courseInfoContext.js ├── .gitignore ├── course.json ├── .github └── workflows │ └── next.yaml ├── next.config.js ├── data ├── course.js └── lesson.js ├── styles ├── variables.css ├── footer.css └── courses.css ├── package.json ├── components ├── header.js ├── layout.js ├── footer.js ├── linkedin.js ├── twitter.js ├── corner.js └── github.js ├── csv └── index.js ├── pages ├── _app.js ├── lessons │ └── [section] │ │ └── [slug].js └── index.js ├── README.md └── LICENSE /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lessons/07-auth/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "lock" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/12-testing/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "flask" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/01-start-here/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "circle-play" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/04-intro-to-prisma/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "database" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/05-data-modeling/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "shapes" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/09-error-handlers/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "times" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/11-perf/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "tachometer-fast" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/08-route-handlers/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "hand-sparkles" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/10-global-config/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "globe-africa" 3 | } 4 | -------------------------------------------------------------------------------- /lessons/06-routes-and-middleware/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "arrows-alt" 3 | } 4 | -------------------------------------------------------------------------------- /public/images/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/author.jpg -------------------------------------------------------------------------------- /lessons/02-api-basics-in-node/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "icon": "cloud", 3 | "title": "API basics in Node.js" 4 | } 5 | -------------------------------------------------------------------------------- /lessons/03-intro-to-express/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Using Express", 3 | "icon": "bolt-lightning" 4 | } 5 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/course-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/course-icon.png -------------------------------------------------------------------------------- /public/images/BRAND-WHearts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/BRAND-WHearts.png -------------------------------------------------------------------------------- /public/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/social-share-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hendrixer/API-design-v4/HEAD/public/images/social-share-cover.jpg -------------------------------------------------------------------------------- /context/headerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const headerContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = headerContext.Provider; 6 | export const Consumer = headerContext.Consumer; 7 | export const Context = headerContext; 8 | -------------------------------------------------------------------------------- /context/courseInfoContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const courseInfoContext = createContext([{}, () => {}]); 4 | 5 | export const Provider = courseInfoContext.Provider; 6 | export const Consumer = courseInfoContext.Consumer; 7 | export const Context = courseInfoContext; 8 | -------------------------------------------------------------------------------- /lessons/12-testing/A-setting-up-test.md: -------------------------------------------------------------------------------- 1 | ## Setting up tests 2 | 3 | We'll be using `jest` and `supertest` to test our API. This ensures our code does what we intended and continues to do so. 4 |
5 | 6 | `npm i supertest @types/supertest jest @types/jest ts-jest` 7 | 8 |
9 | Next, we'll initialize a jest config: 10 |
11 | 12 | `npx ts-jest config:init`. 13 | 14 | We're now ready to test! 15 | -------------------------------------------------------------------------------- /lessons/13-deployment-and-next-steps/B-next-steps.md: -------------------------------------------------------------------------------- 1 | ## Congrats 2 | 3 | You made it! You now know what it feels like to build an API from scratch using Node.js. We've covered a bunch of stuff, but there is still so much more to learn. I challenge you to build out API's for your projects vs reaching for the Backend as a Service instead. The more API's you build, the better you'll become. 4 |
5 | Thank you! 6 | -------------------------------------------------------------------------------- /lessons/01-start-here/A-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction" 3 | description: "The introduction to this course." 4 | --- 5 | 6 | # Hi, 7 | 8 | And welcome to the API Design with v4 course by [Scott Moss][twitter]. 9 | This course website was specifically made for making online courses and optimized for use with [Frontend Masters][fem]. 10 | 11 | [twitter]: https://twitter.com/scotups 12 | [fem]: https://www.frontendmasters.com 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | *.csv 33 | -------------------------------------------------------------------------------- /lessons/01-start-here/C-what-we-are-building.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "What We're Building" 3 | description: "What you'll actually be building in this course" 4 | --- 5 | 6 | Building an API is fun, but what is this API going to do? I find it best to learn by actually building something. 7 | 8 | ## The App 9 | 10 | We'll be building an API for our imaginary ChangeLog app. This app allows a product manager or engineer post product updates for their users. The user needs to be able to read, create, update, and delete product updates. 11 | -------------------------------------------------------------------------------- /lessons/08-route-handlers/B-handlers.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Create handlers for our routes" 3 | --- 4 | 5 | We've handled auth and tackled input validation. All of this work allows our route handlers to focus on one thing: Interacting with the DB without having to worry about anything else. 6 |
7 | Time to roll up our sleeves and create all of the handlers for our remaining routes! Create a file for each resource in the `src/handlers` folder. Place every handler for that resource in the correct file. Just like we did for `src/handlers/user`, where `/user` is a resource. 8 | -------------------------------------------------------------------------------- /course.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Scott Moss", 4 | "company": "Initialized" 5 | }, 6 | "title": "API Design with Node.js V4", 7 | "subtitle": "for Frontend Masters", 8 | "frontendMastersLink": "https://frontendmasters.com/courses/api-design-nodejs-v4/", 9 | "social": { 10 | "linkedin": "willscottmoss", 11 | "github": "hendrixer", 12 | "twitter": "scotups" 13 | }, 14 | "description": "Learn to build APIs with Node.js", 15 | "keywords": ["API", "Node.js", "Prisma", "Database"], 16 | "productionBaseUrl": "/API-design-v4", 17 | "csvPath": "./out/lessons.csv" 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/next.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy NextJS Course Site to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: npm install, export 14 | run: | 15 | npm install 16 | npm run export 17 | - name: Deploy site to gh-pages branch 18 | uses: crazy-max/ghaction-github-pages@v2 19 | with: 20 | target_branch: gh-pages 21 | build_dir: out 22 | # fqdn: sql.holt.courses 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import path from "path"; 3 | 4 | const buffer = readFileSync(path.join(process.cwd(), "./course.json")); 5 | const course = JSON.parse(buffer); 6 | const BASE_URL = course?.productionBaseUrl || ""; 7 | 8 | const config = { 9 | basePath: BASE_URL, 10 | env: { 11 | BASE_URL, 12 | }, 13 | async redirects() { 14 | if (BASE_URL) { 15 | return [ 16 | { 17 | source: "/", 18 | destination: BASE_URL, 19 | basePath: false, 20 | permanent: false, 21 | }, 22 | ]; 23 | } 24 | return []; 25 | }, 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /data/course.js: -------------------------------------------------------------------------------- 1 | import config from "../course.json"; 2 | 3 | const DEFAULT_CONFIG = { 4 | author: { 5 | name: "An Author", 6 | company: "An Author's Company", 7 | }, 8 | title: "A Superb Course", 9 | subtitle: "That Teaches Nice Things", 10 | frontendMastersLink: "", 11 | description: "A nice course for nice people.", 12 | keywords: ["a nice course", "for people", "to learn", "nice things"], 13 | social: { 14 | linkedin: "btholt", 15 | github: "btholt", 16 | twitter: "holtbt", 17 | }, 18 | productionBaseUrl: "/", 19 | }; 20 | 21 | export default function getCourseConfig() { 22 | return Object.assign({}, DEFAULT_CONFIG, config); 23 | } 24 | -------------------------------------------------------------------------------- /lessons/03-intro-to-express/A-express-enters-the-chat.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "What is Express.js" 3 | description: "What is Express.js and why would you want to use it." 4 | --- 5 | 6 | ## The O.G. API framework 7 | 8 | **Express.js** is a really neat and highly adopted framework for Node.js that makes it trivial to build an API. It's by far the most popular one due to it being created near Node.js' time of creation and how much support the community has poured into it. Express is synomous with Django for Python, Sinatra for Ruby, Spring for Java, and Gin for Go Lang 9 | 10 | ## Other options 11 | 12 | There are many other frameworks, some faster, some newer. Many of them have been inspired by or even built on top of Express.js. It's also the one I know the most about 😄. 13 | -------------------------------------------------------------------------------- /styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #673ab7; 3 | --secondary: #64ffda; 4 | --highlight: white; 5 | 6 | --text-lesson-body: #f4f4f4; 7 | --text-header: var(--primary); 8 | --text-main-headers: var(--highlight); 9 | --text-links: #007bff; 10 | --text-footer: #333; 11 | 12 | --bg-lesson-preface: #333333ff; 13 | --bg-main: #000000ff; 14 | --bg-dots: var(--bg-main); 15 | --bg-lesson: #111111ff; 16 | 17 | --nav-buttons: var(--highlight); 18 | --nav-buttons-hover: var(--secondary); 19 | --nav-buttons-text: var(--bg-main); 20 | 21 | --corner-active: var(--highlight); 22 | --corner-inactive: var(--bg-main); 23 | --icons: var(--highlight); 24 | --footer-icons: var(--highlight); 25 | 26 | --emphasized-bg: #333333ff; 27 | --emphasized-border: red; 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "license": "(CC-BY-NC-4.0 OR Apache-2.0)", 5 | "author": "Scott Moss ", 6 | "scripts": { 7 | "dev": "next dev -p 3001", 8 | "build": "next build && npm run csv", 9 | "export": "next build && next export && npm run csv", 10 | "start": "next start", 11 | "csv": "node csv/index.js" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-free": "^6.1.1", 15 | "gray-matter": "^4.0.3", 16 | "highlight.js": "^11.5.1", 17 | "marked": "^4.0.16", 18 | "next": "^12.1.6", 19 | "react": "^18.1.0", 20 | "react-dom": "^18.1.0", 21 | "title-case": "^3.0.3" 22 | }, 23 | "devDependencies": { 24 | "convert-array-to-csv": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lessons/06-routes-and-middleware/C-errata-nodemon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Errata - Nodemon Setup" 3 | description: "Auto-restart the server with Nodemon" 4 | --- 5 | 6 | ## Nodemon 7 | 8 | Throughout the course, Scott is manually stopping and starting the server anytime changes in the source code occurs. This was an intential decision to reduce the complexity of the setup. However, if you want your server to auto-restart when there is a change, you can use [nodemon](https://nodemon.io/). 9 | 10 | ### Nodemon Setup Instructions 11 | 12 | Install nodemon 13 | 14 | ```bash 15 | npm install nodemon --save-dev 16 | ``` 17 | 18 | > nodemon is a installed as a development dependency because, once the application is deployed, the web host will be responsible for starting the server. 19 | 20 | Once nodemon is installed, update the `dev` script in your package.json file 21 | 22 | ```json 23 | "dev": "nodemon src/index.ts" 24 | ``` 25 | -------------------------------------------------------------------------------- /styles/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | padding: 50px 15px; 4 | background-color: var(--primary); 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | color: var(--text-footer); 9 | } 10 | 11 | .socials { 12 | display: flex; 13 | align-items: center; 14 | max-width: 900px; 15 | width: 100%; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .social { 21 | display: inline-block; 22 | list-style: none; 23 | margin-right: 40px; 24 | } 25 | 26 | .social img:hover { 27 | opacity: 0.4; 28 | } 29 | 30 | .social img { 31 | transition: opacity 0.25s; 32 | width: 30px; 33 | } 34 | 35 | .terms { 36 | font-size: 10px; 37 | } 38 | 39 | .terms p { 40 | margin: 3px; 41 | } 42 | 43 | .footer a { 44 | color: inherit; 45 | text-decoration: underline; 46 | } 47 | 48 | .social svg { 49 | transition: opacity 0.25s; 50 | } 51 | 52 | .social svg:hover { 53 | opacity: 0.4; 54 | } 55 | -------------------------------------------------------------------------------- /lessons/05-data-modeling/A-design-a-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "How to design a schema" 3 | --- 4 | 5 | We're not building a UI for this API in this course. However, having a UI design is helpful when designing your models. Knowing what data is required in a UI gives you hints on what needs to be recorded in a DB. 6 |
7 | 8 | We'll be using the design [here](https://www.framer.com/templates/chronos/) as an imaginary ChangeLog app. Let's observe this UI and figure out what resources we need to record in our DB. 9 | 10 |
11 | So it looks like we'll need at least the following: 12 | * `Update` - title, body, asset, status (in progress, launched), created at, and version 13 | * `Update Point` - belongs to an update, type (feature, improvement, bug) 14 | * `Feature` 15 |
16 | And of course standard things like users. There will probably be supporting models that we create to help with querying and other logic like authentication. 17 | -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import Link from "next/link"; 3 | import { Context as HeaderContext } from "../context/headerContext"; 4 | import { Context as CourseContext } from "../context/courseInfoContext"; 5 | 6 | export default function Header(props) { 7 | const [{ section, title, icon }] = useContext(HeaderContext); 8 | const { frontendMastersLink } = useContext(CourseContext); 9 | return ( 10 |
11 |

12 | {props.title} 13 |

14 |
15 | {frontendMastersLink ? ( 16 | 17 | Watch on Frontend Masters 18 | 19 | ) : null} 20 | {section ? ( 21 |

22 | {section} {title} 23 |

24 | ) : null} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lessons/13-deployment-and-next-steps/A-deploying.md: -------------------------------------------------------------------------------- 1 | We now need to deploy our API so we can use it! We just need to consider a few things. There really aren't too many things we need to change right now to make sure we can deploy. It also depends on where you're deploying. 2 | 3 |
4 | For us, the most imporant thing is making sure our repo is on Github and we create a build script to build our TypeScript. 5 |
6 | In our package.json: 7 | 8 | ```json 9 | "scripts": { 10 | "build": "tsc -p tsconfig.json", 11 | "start": "node dist/index.js", 12 | } 13 | ``` 14 | 15 | Render.com will use these scripts to build and start our server. Last thing is one final adjustment to our tsconfig. 16 | 17 | ```json 18 | { 19 | "compilerOptions": { 20 | "sourceMap": true, 21 | "outDir": "./dist", 22 | "rootDir": "./src", 23 | "strict": false, 24 | "lib": ["esnext"], 25 | "esModuleInterop": true, 26 | "declaration": true 27 | }, 28 | "include": ["src/**/*.ts"] 29 | } 30 | ``` 31 | 32 | We're ready to deploy! 33 | -------------------------------------------------------------------------------- /lessons/06-routes-and-middleware/B-middleware.md: -------------------------------------------------------------------------------- 1 | Middleware are functions that run right before your handlers run. They can do things like augment the request, log, handle errors, authenticate, and pretty much anything else. They look exactly like a handler with one difference. Because you can have a list of middleware, there needs to be a mechanism to move into the next middlware function when work is done in the current middleware. It looks like this: 2 | 3 | ```ts 4 | const myMiddlware = (req, res, next) => { 5 | // ... do my work, and when I done call next() 6 | next(); 7 | }; 8 | ``` 9 | 10 | This `next` function is exactly what it sounds like. It tells Express that we're done in the middleware and it's safe to proceed to whatever is next (more middleware or a handler). 11 |
12 | To apply the middleware to a route, you can do this: 13 | 14 | ```ts 15 | app.get("/todo/:id", myMiddleware, my2ndMiddleware, handler); 16 | // or 17 | app.get("/todo/:id", [myMiddleware, my2ndMiddleware], handler); 18 | ``` 19 | 20 |
21 | Middleware will run in the order in which you passed them as arguments. 22 | -------------------------------------------------------------------------------- /lessons/05-data-modeling/C-first-migrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Migrating the DB" 3 | --- 4 | 5 | ## Migrations 6 | 7 | Since this is our first time interacting with the DB, we need to run our initial migration to get the DB and our schema in sync. We'll continue to run migrations as we make schema changes to ensure the schema and any data in the DB stay in sync. Before we run a migration, we need to install the prisma client which is the SDK we'll use in our code to interact with the DB. This client is type-safe and based on of our schema. It's actually an NPM package that gets generated on demand to adjust to your schema! Pretty cool. 8 |
9 | `npm i @prisma/client --save` 10 |
11 | Next, lets migrate the DB. Make sure you added your DB connection string to the `.env` file as `DATABASE_URL`. You can find the connection string on render. Be sure to use the external one. Now to run the migration: 12 |
13 | `npx prisma migrate dev --name init` 14 |
15 | This will migrate the DB over to use our schema and then generate the new client for us. This client will be used in our code and is now type-checked against our schema. 16 | -------------------------------------------------------------------------------- /lessons/03-intro-to-express/B-easy-mode-with-express.md: -------------------------------------------------------------------------------- 1 | Lets create an API with Express instead of the vanilla JavaScript one we created earlier. 2 | 3 | ## Dependencies 4 | 5 | Before we can use Express, we need to install it. Using NPM or Yarn: 6 | 7 | `npm i express --save` 8 | or 9 | `yarn add express` 10 | 11 | Next, let's create a simple API! 12 | 13 | ```javascript 14 | const express = require("express"); 15 | const app = express(); 16 | const port = 5000; 17 | const path = require("path"); 18 | 19 | app.use(express.static("static")); 20 | 21 | /** 22 | * app.[method]([route], [route handler]) 23 | */ 24 | app.get("/", (req, res) => { 25 | // sending back an HTML file that a browser can render on the screen. 26 | res.sendFile(path.resolve("pages/index.html")); 27 | }); 28 | 29 | // creates and starts a server for our API on a defined port 30 | app.listen(port, () => { 31 | console.log(`Example app listening at http://localhost:${port}`); 32 | }); 33 | ``` 34 | 35 | Express literally gives us a framework to build out the business logic of our APIs without having to put too much thought into how to make the API functional in the first place. 36 | -------------------------------------------------------------------------------- /components/layout.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import Footer from "./footer"; 4 | import Header from "./header"; 5 | import getCourseConfig from "../data/course"; 6 | import { Provider as HeaderProvider } from "../context/headerContext"; 7 | import { Provider as CourseInfoProvider } from "../context/courseInfoContext"; 8 | 9 | function Layout({ children }) { 10 | const courseInfo = getCourseConfig(); 11 | const headerHook = useState({}); 12 | return ( 13 | 14 | 15 |
16 |
17 |
18 |
{children}
19 |
20 |
25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default function App({ children }) { 32 | return {children}; 33 | } 34 | -------------------------------------------------------------------------------- /lessons/09-error-handlers/B-custom-handler.md: -------------------------------------------------------------------------------- 1 | We can overwrite the default handler with our own custom one. 2 | 3 | ```ts 4 | app.use((err, req, res, next) => { 5 | if (err.type === "auth") { 6 | res.status(401); 7 | res.json({ message: "nope" }); 8 | } 9 | }); 10 | ``` 11 | 12 | This is an example of what a custom error handler might look like. If you use any error reporting service to capture and analyze errors, here is where you want to report your errors. We can even have many custom handlers all doing different things. For example, one might log the error, then another reports it to your reporting service, and the last one actually responds to the request. 13 |
14 | We can augment an error however we see fit before passing it to `next`. This will help our custom handler decide on how to handle the actuall error. 15 | 16 | ```ts 17 | catch (e) { 18 | e.type = 'input' 19 | next(e) 20 | } 21 | 22 | app.use((err, req, res, next) => { 23 | if (err.type === 'input') { 24 | res.status(400) 25 | return res.send('invalid input') 26 | } 27 | }) 28 | ``` 29 | 30 | ## Handle our errors 31 | 32 | Next, lets go through our handlers and middleware and make sure we're handling our errors. Also, we must create our own error handler for the app. 33 | -------------------------------------------------------------------------------- /lessons/08-route-handlers/A-validating-inputs.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Input validation for express requests" 3 | --- 4 | 5 | Never trust the user! Words to live by when working with user input. Especially more true for an API that is responsible for holding the weight of every client. The last thing you want is for a user's input choice to crash your entire server. We want to get ahead of that and validate all incoming data for our API. 6 |
7 | So what does input validation look like? We'll be using a package to help us do that. 8 |
9 | `npm i express-validator --save` 10 |
11 | For any route, you want to add input validations: 12 | 13 | ```ts 14 | import { body, validationResult } from "express-validator"; 15 | 16 | app.post("/product", body("name").isString(), (req, res) => { 17 | const errors = validationResult(req); 18 | 19 | if (!errors.isEmpty()) { 20 | res.status(400); 21 | res.json({ errors: errors.array() }); 22 | } 23 | }); 24 | ``` 25 | 26 | We can use the provided middleware to create new validations against any user input. This includes the body, headers, cookies, params, and query string. In this example, we're validating that the request includes a `name` field on the body. 27 |
28 | Let's apply input validations to all our `put` and `post` requests. 29 | -------------------------------------------------------------------------------- /lessons/10-global-config/B-configs.md: -------------------------------------------------------------------------------- 1 | Let's create a new file `src/config/index.ts`. Then, for every environment, we'll create a file. Those enviroments are local, staging, and production. 2 |
3 |
4 | 5 | So create, `src/config/local.ts`, `src/config/staging.ts`, `src/config/prod.ts`. Each of one these files will be used to configure variables for their matching environment. 6 | 7 |
8 | Next, we'll merge the configs together, giving us our final config that we can use anywhere. 9 |
10 | In `src/config/index.ts` 11 | 12 | ```ts 13 | import merge from "lodash.merge"; 14 | 15 | // make sure NODE_ENV is set 16 | process.env.NODE_ENV = process.env.NODE_ENV || "development"; 17 | 18 | const stage = process.env.STAGE || "local"; 19 | let envConfig; 20 | 21 | // dynamically require each config depending on the stage we're in 22 | if (stage === "production") { 23 | envConfig = require("./prod").default; 24 | } else if (stage === "staging") { 25 | envConfig = require("./staging").default; 26 | } else { 27 | envConfig = require("./local").default; 28 | } 29 | 30 | const defaultConfig = { 31 | stage, 32 | dbUrl: process.env.DATABASE_URL, 33 | jwtSecret: process.env.JWT_SECRET, 34 | port: process.env.PORT, 35 | logging: false, 36 | }; 37 | 38 | export default merge(defaultConfig, envConfig); 39 | ``` 40 | -------------------------------------------------------------------------------- /lessons/11-perf/A-async-everything.md: -------------------------------------------------------------------------------- 1 | Node.js is single threaded by default. A side effect of this is that your code could potentially be blocking the main execution thread. On a server that is shared by all your clients, this is really bad. 2 |
3 | Your API could fail to take incoming request because it's blocked by CPU intensive work on the main thread. To avoid this, make sure any intense workload is asynchronous. 4 | 5 | ## Blocking code 6 | 7 | Here's an example of some code that would prevent your API from receiving more requests until its done. 8 | 9 | ```ts 10 | import fs from "fs"; 11 | 12 | const result = fs.readFileSync("some/path/to/file.txt"); 13 | ``` 14 | 15 | Reading a file with the sync version of the method is blocking. If that file was huge and running on a popular route where many requests triggered its execution, your API will eventually slow down. To avoid this, you'd want to make sure this code was async. 16 | 17 | ```ts 18 | // promise version 19 | import fs from "fs/promises"; 20 | 21 | const result = await fs.readFile("some/path/to/file.txt"); 22 | ``` 23 | 24 | Now, this code will no longer tie up the main thread, allowing more requests to come through. If you ABSOLUTELY could not convert some sync code to async code, then you should use a child process to run the code on a different thread. 25 | -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Gh from "./github"; 3 | import Tw from "./twitter"; 4 | import Li from "./linkedin"; 5 | 6 | export default function Footer({ twitter, linkedin, github }) { 7 | return ( 8 |
9 |
    10 | {twitter ? ( 11 |
  • 12 | 13 | 14 | 15 |
  • 16 | ) : null} 17 | {github ? ( 18 |
  • 19 | 20 | 21 | 22 |
  • 23 | ) : null} 24 | {linkedin ? ( 25 |
  • 26 | 27 |
  • 28 | 29 |
  • 30 | ) : null} 31 |
  • 32 |
    33 |

    Content Licensed Under CC-BY-NC-4.0

    34 |

    Code Samples and Excercises Licensed Under Apache 2.0

    35 |

    36 | Site Designed by{" "} 37 | Alex Danielson 38 |

    39 |
    40 |
  • 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /csv/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { convertArrayToCSV } from "convert-array-to-csv"; 4 | import { getLessons } from "../data/lesson.js"; 5 | 6 | async function start() { 7 | const configBuffer = await fs.readFile( 8 | path.join(process.cwd(), "course.json") 9 | ); 10 | const config = JSON.parse(configBuffer); 11 | 12 | if (!config.csvPath) { 13 | console.log("no csvPath in course.json, skipping CSV generation"); 14 | return; 15 | } 16 | 17 | process.env.BASE_URL = config?.productionBaseUrl || ""; 18 | const sections = await getLessons(); 19 | 20 | const lessons = []; 21 | 22 | for (let i = 0; i < sections.length; i++) { 23 | const section = sections[i]; 24 | 25 | for (let j = 0; j < section.lessons.length; j++) { 26 | const lesson = section.lessons[j]; 27 | 28 | lessons.push({ 29 | order: lesson.order, 30 | sectionTitle: section.title, 31 | lessonTitle: lesson.title, 32 | slug: section.slug + "/" + lesson.slug, 33 | sectionIcon: section.icon, 34 | filePath: lesson.fullSlug, 35 | description: lesson.description, 36 | }); 37 | } 38 | } 39 | 40 | const csv = convertArrayToCSV(lessons); 41 | 42 | await fs.writeFile(config.csvPath, csv); 43 | console.log(`wrote ${lessons.length} rows to ${config.csvPath}`); 44 | } 45 | 46 | start(); 47 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import "@fortawesome/fontawesome-free/css/all.css"; 3 | 4 | import "highlight.js/styles/monokai-sublime.css"; 5 | import "../styles/variables.css"; 6 | import "../styles/footer.css"; 7 | import "../styles/courses.css"; 8 | 9 | import Layout from "../components/layout"; 10 | 11 | export default function App({ Component, pageProps }) { 12 | return ( 13 | 14 | 15 | 20 | 26 | 32 | 38 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /lessons/09-error-handlers/A-error-handler.md: -------------------------------------------------------------------------------- 1 | If an error is not caught on our server, it will crash and our API will non functional. 2 | To avoid this, we want to make sure we catch any and all potential errors. We also want to do right by the requester and inform them on any errors, especially if it's their fault. Express uses its middleware system to ship a default error handler that does a good job at catching any syncronous errors that throw inside middleware and handlers. Let's test that: 3 | 4 | ```ts 5 | app.get("/", () => { 6 | throw new Error("oops"); 7 | }); 8 | ``` 9 | 10 | If you were to run this, you will see an error in your terminal, however, the server will not crash. Express is adding a default error handler to the bottom of our router stack. Error handling middleware is just like all middleware except they don't run before a handler, they only run after an error has been thrown. 11 | 12 | ```ts 13 | app.use((err, req, res, next) => { 14 | // handle error 15 | }); 16 | ``` 17 | 18 | As long as your error handler is registered, it wull catch errors. Now, what about async errors? Well, we have to tell Express about our async errors. Inside our handlers and middleware: 19 | 20 | ```ts 21 | const handler = async (req, res, next) = { 22 | // .... 23 | try { 24 | const user = await prisma.user.create({}) 25 | } catch(e) { 26 | next(e) 27 | } 28 | } 29 | ``` 30 | 31 | Anything we pass to `next` is considered an error. 32 | -------------------------------------------------------------------------------- /lessons/04-intro-to-prisma/B-db-setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DB Setup" 3 | description: "Setup our DB and install Prisma" 4 | --- 5 | 6 | ## Psql 7 | 8 | We'll be using PSQL as a DB in this course. You won't have to install anything as we'll be using a hosting and managed DB from [Render](https://render.com). Go there, create an account, and then create a FREE psql DB. 9 | 10 | ## Installing Prisma 11 | 12 | Prisma works best when you're using a TypeScript. So in addition to installing Prisma, we're going to convert our app to TypeScript. Don't worry if you don't know TypeScript. We won't be doing all the fancy typed stuff in this course. We just want that sweet autocomplete for our DB interactions through Prisma. Trust me, its magical ✨. On to the installing! 13 |
14 |
15 | `npm i typescript ts-node @types/node prisma --save-dev` 16 |
17 |
18 | Then create a `tsconfig.json` file which is the config file for TypeScript. Add this to that file: 19 |
20 | 21 | ```json 22 | { 23 | "compilerOptions": { 24 | "sourceMap": true, 25 | "outDir": "dist", 26 | "strict": true, 27 | "lib": ["esnext"], 28 | "esModuleInterop": true 29 | } 30 | } 31 | ``` 32 | 33 |
34 |
35 | Next, we'll initalize Prisma 36 |
37 | 38 | `npx prisma init` 39 | 40 |
41 | This command will do a few things: 42 | 43 | - Create a prisma folder 44 | - Create a schema file in that folder 45 | Next, we'll learn how to design and create some models in our schema 46 | -------------------------------------------------------------------------------- /lessons/10-global-config/A-env-and-stage.md: -------------------------------------------------------------------------------- 1 | As your API gets bigger, it's harder to keep track of things like secrets, options, env vars, etc., especially across different environments. We need want to be able to give our app the flexibility to adapt to each environment (local, staging, prod, etc) without having to change too much. It would be awesome, if everything we needed to change was in one place that we could import everywhere. 2 | 3 | ## NODE_ENV 4 | 5 | Environment variables are values provided from the environment at run time. They're perfect for injecting secret values your server needs, but are too dangerous to store in git. 6 |
7 | One very important env var is `NODE_ENV`. This env var is usually tasked with determining the "mode" your app is running. Some examples are: 8 | 9 | - `development` (default) 10 | - `production` 11 | - `testing` 12 | 13 |
14 | 15 | Some packages like `React` behave differently, depending on the value of this env var. We may also want our code to use different values as well. For instance, if we have analytics on our API, we probably don't want to have that enabled while we're developing locally. So we can check the `NODE_ENV` to see what env we are in and conditionally track based on the environment. 16 |
17 | This is very powerful! However, having a bunch of the same conditionals sprinkled across our app can make it tough when we need to make changes. That's why we're going to centralized all the dynamic values of our app that depend on the current env. This way, we only have to change one file to completely change how our app works. 18 | -------------------------------------------------------------------------------- /components/linkedin.js: -------------------------------------------------------------------------------- 1 | export default function LinkedIn() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lessons/02-api-basics-in-node/A-vanilla-server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Vanilla API" 3 | description: "Build a vanilla, no framework used, API in node.js" 4 | --- 5 | 6 | ## Playing on hard mode 7 | 8 | Node.js comes with everything you need to build a fully functional API. However, it's very tedious and unwise to use the raw modules. For context and appreciation of frameworks, let's do it anyway! 9 | 10 | ```javascript 11 | import http from "http"; 12 | 13 | const server = http.createServer(async (req, res) => { 14 | if (req.url === "/" && req.method === "GET") { 15 | res.writeHead(200, { "Content-Type": "application/json" }); 16 | res.write(JSON.stringify({ message: "hello" })); 17 | 18 | res.end(); 19 | return; 20 | } 21 | 22 | res.writeHead(404, { "Content-Type": "application/json" }); 23 | res.end(JSON.stringify({ message: "nope" })); 24 | }); 25 | 26 | const PORT = process.env.PORT; 27 | 28 | server.listen(PORT, () => { 29 | console.log(`server on ${PORT}`); 30 | }); 31 | ``` 32 | 33 | ... and just like that, we have an API. It doesn't do much functionally, but it works. A client can issue a `GET` request to the server at `/` and get back some JSON. Any other request will yield a different message and a `404` status code. We'll talk about HTTP Methods, status codes, and routes later in the course. 34 | 35 | ## Why this breaks down 36 | 37 | When building something trivial like our example, then not using a framework is fine. Maybe even preferred. But you'll have to start creating your abstractions as soon as you build anything. Why create your own when a framework is just that: Abstractions based on some opinions that benefit from having community support. 38 | -------------------------------------------------------------------------------- /lessons/01-start-here/B-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tools" 3 | description: "What tools we will be using for this course" 4 | --- 5 | 6 | # Tools 7 | 8 | When it comes to building out production-ready API's, there are several moving pieces, and for each one, there are several options. We won't be covering EVERY single detail in this course. Instead we'll cover the common tools that you'll need. 9 | 10 | ## Runtime 11 | 12 | We'll be using **Node.js** for this course. Why? Node.js uses JS as the language of choice. If you've worked on web apps, then you know JS. Node.js has a rich, active ecosystem as well. I also know Node.js the best. Alternatives include Ruby, Python, Java, Go, Rust, and many more. 13 | 14 | ## Framework 15 | 16 | We could create an API without a framework, but that wouldn't be the best use of our time and isn't taking advantage of an amazing ecosystem at our fingertips. So, we'll be using **Express** to create our API inside of Node.js. 17 | 18 | ## Database 19 | 20 | There are so many great options when choosing a database these days. We'll be using **Psql** or _Postgres_. It's one of the most popular DBs in the world and gives us tons of options when it's time to deploy our API. For the ORM, we'll use Prisma to interact with our DB. Prisma has proven to be a very valuable tool that can create schemas, query our DB, and even handle migrations. It also works with a variety of databases. 21 | 22 | ## Hosting 23 | 24 | When it comes to hosting a Node.js based API, you can pretty much close your eyes then point in any direction and you'll be sure to land on a platform that supports Node. This is not a DevOps class, so we want to use a platform that manages it all for us. For that, we'll be using **Render**. 25 | -------------------------------------------------------------------------------- /components/twitter.js: -------------------------------------------------------------------------------- 1 | export default function Twitter() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /lessons/04-intro-to-prisma/A-what-is-prisma.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "What Prisma" 3 | description: "What is Prisma and why do we need it" 4 | --- 5 | 6 | ## You need an ORM 7 | 8 | When it comes to choosing a DB for your API, there are many variables at play. For the most part, you'll end up with a Relational (SQL) DB or a NoSql DB (Document Store). We're not going to get into what is the "best" DB because that's impossible to answer and changes as your product's needs change. 9 | 10 | However, no matter the DB, how you interact with the DB matters. What good is the perfect DB that is painfull to interact with. Enter, and ORM. Object-Relational Mapper (ORM) is a term used to describe a technique that allows you to interact with a DB using an object-oriented approach. When most people say ORM, they're actually talking about an ORM library, which is really just and SDK for your DB. For example, without and ORM, you can only interact with a SQL DB using SQL. 11 | 12 | ```sql 13 | INSERT INTO Customers ( 14 | CustomerName, 15 | ContactName, 16 | Address, 17 | City, 18 | PostalCode, 19 | Country 20 | ) 21 | VALUES 22 | ('Cardinal', 23 | 'Tom B. Erichsen', 24 | 'Skagen 21', 25 | 'Stavanger', 26 | '4006', 27 | 'Norway' 28 | ); 29 | ``` 30 | 31 |
32 | Using an ORM, depending on which one, your DB interaction for the same logic might look like this. 33 | 34 | ```javascript 35 | db.customers.create({ 36 | customerName: 'Cardinal', 37 | contactName: 'Tom B. Erichsen', 38 | address: 'Skagen 21', 39 | .... 40 | }) 41 | ``` 42 | 43 | You tell me: What looks easier to work with? 44 |
45 |
46 | **Exactly.** 47 |
48 |
49 | 50 | ## What is Prisma 51 | 52 | Prisma is a DB agnostic, type safe ORM. It supports most DBs out there. It not only has an SDK for doing basic and advanced querying of a DB, but also handles schemas, migrations, seeding, and sophisticated writes. It's slowly but surely becoming the ORM of choice for Node.js projects. 53 | -------------------------------------------------------------------------------- /lessons/12-testing/B-types-of-tests.md: -------------------------------------------------------------------------------- 1 | ## Unit test 2 | 3 | A unit test is all about testing individual pieces of logic independently of each other. You have to make sure you write your code in a way that can be unit tested. 4 | 5 | ```ts 6 | // not testable 7 | const value = 100; 8 | const action = () => { 9 | console.log(value); 10 | }; 11 | ``; 12 | ``` 13 | 14 | ```ts 15 | // testable 16 | export const action = (value) => { 17 | console.log(value); 18 | }; 19 | ``` 20 | 21 | Using arguments vs creating closures and exporting your code are all great patterns to use when creating testable code. Now what does a unit test look like? 22 | 23 | ```ts 24 | describe("user handler", () => { 25 | it("should do a thing", async () => { 26 | // .,... 27 | 28 | expect("something").toBe("something"); 29 | }); 30 | }); 31 | ``` 32 | 33 | This is how you might write a unit test in Jest. Each `it` block is an actual test where you usually call some function you want to test, and then create some assertion about what its return value should be. The `describe` function is just for organizing your test. 34 | 35 | ## Integraion test 36 | 37 | Integration tests will test how an entire route works by actually making a request to observe what the API sent back and making assertions on that result. We can use jest along with supertest to run integration test. 38 | 39 | ```ts 40 | import app from "../server"; 41 | import request from "supertest"; 42 | 43 | describe("POST /user", function () { 44 | it("responds with json", async () { 45 | const res = await request(app) 46 | .post("/user") 47 | .send({ username: "hello", password: "hola" }) 48 | .set("Accept", "application/json") 49 | 50 | expect(res.headers["Content-Type"]).toMatch(/json/); 51 | expect(res.status).toEqual(200); 52 | }); 53 | }); 54 | ``` 55 | 56 | In this test, we're using the `request` from supertest to make a request to `POST /user`, which in our app creates a user. We're sending up the required payload and expect to get a successful 200 when its done. 57 |
58 |
59 | Now, go write some tests! 60 | -------------------------------------------------------------------------------- /components/corner.js: -------------------------------------------------------------------------------- 1 | export default function Corner() { 2 | return ( 3 |
4 | 11 | 12 | 13 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/github.js: -------------------------------------------------------------------------------- 1 | export default function GitHub() { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /lessons/07-auth/B-users.md: -------------------------------------------------------------------------------- 1 | We know from our schema that a user needs a unique username and password. Lets create a handler to create a user. Before we can do that, we'll create some helper functions to hash and compare a user's password so we're not storing it in plain text. 2 |
3 | Inside of `src/modules/auth.ts` 4 | 5 | ```ts 6 | import * as bcrypt from "bcrypt"; 7 | 8 | export const comparePasswords = (password, hash) => { 9 | return bcrypt.compare(password, hash); 10 | }; 11 | 12 | export const hashPassword = (password) => { 13 | return bcrypt.hash(password, 5); 14 | }; 15 | ``` 16 | 17 | `comparePasswords` compare a plain text password and hashed password to see if they're the same. 18 |
19 | 20 | `hashPassword` hashes a password. 21 |
22 | Now, let's create that handler inside `src/handlers/user.ts` 23 | 24 | ```ts 25 | import prisma from "../db"; 26 | import { createJWT, hashPassword } from "../modules/auth"; 27 | 28 | export const createNewUser = async (req, res) => { 29 | const hash = await hashPassword(req.body.password); 30 | 31 | const user = await prisma.user.create({ 32 | data: { 33 | username: req.body.username, 34 | password: hash, 35 | }, 36 | }); 37 | 38 | const token = createJWT(user); 39 | res.json({ token }); 40 | }; 41 | ``` 42 | 43 | First thing here is the prisma import. I'm creating module that exports a Prisma client so we don't have to keep creating a new client every time we need it. 44 |
45 | There isn't anything special going on here other than creating a new user then using that user to create a JWT and sending that token back as a response. 46 |
47 | Next, we need to allow a user to sign in. 48 | 49 | ```ts 50 | export const signin = async (req, res) => { 51 | const user = await prisma.user.findUnique({ 52 | where: { username: req.body.username }, 53 | }); 54 | 55 | const isValid = await comparePasswords(req.body.password, user.password); 56 | 57 | if (!isValid) { 58 | res.status(401); 59 | res.send("Invalid username or password"); 60 | return; 61 | } 62 | 63 | const token = createJWT(user); 64 | res.json({ token }); 65 | }; 66 | ``` 67 | 68 | Using the provided username, we search for a matching user. We'll get more into how to query with Prisma soon. Then we compare passwords. If it's a match, we create a JWT and send it back. 69 | 70 |
71 | Now we need to create some routes and add these handlers. We can do this in `src/server.ts` 72 | 73 | ```ts 74 | import { createNewUser, signin } from "./handlers/user"; 75 | 76 | app.post("/user", createNewUser); 77 | app.post("/signin", signin); 78 | ``` 79 | -------------------------------------------------------------------------------- /pages/lessons/[section]/[slug].js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { getLesson, getLessons } from "../../../data/lesson"; 4 | import getCourseConfig from "../../../data/course"; 5 | import Corner from "../../../components/corner"; 6 | import { Context } from "../../../context/headerContext"; 7 | 8 | export default function LessonSlug({ post }) { 9 | const courseInfo = getCourseConfig(); 10 | const [_, setHeader] = useContext(Context); 11 | useEffect(() => { 12 | setHeader({ 13 | section: post.section, 14 | title: post.title, 15 | icon: post.icon, 16 | }); 17 | return () => setHeader({}); 18 | }, []); 19 | 20 | const title = post.title 21 | ? `${post.title} – ${courseInfo.title}` 22 | : courseInfo.title; 23 | const description = post.description 24 | ? post.description 25 | : courseInfo.description; 26 | 27 | return ( 28 | <> 29 | 30 | {title} 31 | 32 | {/* */} 33 | 34 | 35 | 39 | 40 | 41 |
42 |
43 |
47 |
48 | {post.prevSlug ? ( 49 | 50 | ← Previous 51 | 52 | ) : null} 53 | {post.nextSlug ? ( 54 | 55 | Next → 56 | 57 | ) : null} 58 |
59 |
60 | 61 |
62 | 63 | ); 64 | } 65 | 66 | export async function getStaticProps({ params }) { 67 | const post = await getLesson(params.section, params.slug); 68 | return { 69 | props: { 70 | post, 71 | }, 72 | }; 73 | } 74 | 75 | export async function getStaticPaths() { 76 | const sections = await getLessons(); 77 | const lessons = sections.map((section) => section.lessons); 78 | const slugs = lessons.flat().map((lesson) => lesson.fullSlug); 79 | 80 | return { paths: slugs, fallback: false }; 81 | } 82 | -------------------------------------------------------------------------------- /lessons/02-api-basics-in-node/B-api-anatomy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "API Anatomy" 3 | --- 4 | 5 | ## Little bits 6 | 7 | Every API ever shares a common make up. Doesn't matter the language or the environment. What makes them different is how each one does it and what they build on top if. Let's talk about all those common bits that make up an API. 8 | 9 | ## Server 10 | 11 | An API is a **Server**. This may seem obvious, but really needs to be understood. A server is an app that has no visual representation and is always running. Usually connected to a network and shared among many clients (UIs, web apps, mobile apps, other servers, etc). Servers usually sit in front of a DB and facilitate access to that DB. There are small exceptions here, and that would be Serverless APIs. Big difference with serverless APIs is they are not always on like a traditional server. Servers must operate on a port, a virtual place on a computers operating system where network connections start and end. Ports help computers sort out their network traffic. A server must also have an IP address, a unique location used to locate a server on a network, like the internet. An IP address helps traffic go to and from a specific device whereas a port allows targeting of specific services or apps on a device. An example would look like this: 12 | `127.0.0.1:5000`. Where `5000` is the port and the rest is the IP address. 13 | 14 | ## Route 15 | 16 | A route is a unique combination of a URL path and a HTTP Method. Routes are used to locate certain resources or trigger certain actions on an API. 17 | 18 | HTTP Methods or Verbs are constants that are used by API developers and HTTP to help determine intent of an API call. There are many methods, but the common ones are: 19 | 20 | - `GET` - used to get information from an API 21 | - `POST` - used to mutate or create new information on an API. Usually has data sent along with the request. 22 | - `PUT` - used to replace existing information on an API. Usually has data sent along with the request. 23 | - `PATCH` - used to update existing information on an API. Usually has data sent along with the request. 24 | - `DELETE`- used to remove existing information on an API. 25 | - `OPTIONS` - used with CORS by browsers to check to see if the client is able to actually communicate with an API 26 | 27 | Here are some examples of routes: 28 | 29 | - `GET /api/user/1` 30 | - `POST /food` 31 | 32 | Engineers can design these routes and what the routes actually do however they see fit. To standardize this, there are different approaches to designing these routes. The most popular is REST. There are others like grpc, graphql, and protobuff. 33 | 34 | ## Route Handlers 35 | 36 | A route handler is a function that executes when a certain route is triggered from an incoming request. Depending on the API design and intent of the request, the handler will interact with the database. Looking at our route examples above: 37 | 38 | - `GET /api/user/1` - if this API was a REST API, the route handler would query the database for a user with the ID of 1. 39 | 40 | - `POST /food` - if this API was a REST API, the route handler would create a new food in the database, using the data sent along with the request. 41 | -------------------------------------------------------------------------------- /lessons/07-auth/A-auth-middleware.md: -------------------------------------------------------------------------------- 1 | We don't want just anyone using our API. Our DB is multi-tenat, so we need to identify what user is making the request so we can scope their queries and writes to the user. We don't want one user having access to another user's data. 2 |
3 | To ensure all of this, we're going to protect our API. Tokens are a great approach for this. Things like API Keys and JWT's are good examples of tokens. You could also use Sessions. We're going to use JWTs. 4 | 5 | ## Creating a JWT 6 | 7 | Lets create a function that create's JWTs for when a new user signups up or current one signs in. Users will need to send the JWT on every single request to get access to the API. Our API never stores a JWT, its stored client side. 8 |
9 | 10 | We need to install a few things: 11 | 12 |
13 | 14 | `npm i jsonwebtoken bcrypt dotenv` 15 |
16 | 17 | Create a new file `src/modules/auth` and add this: 18 | 19 | ```ts 20 | import jwt from "jsonwebtoken"; 21 | 22 | export const createJWT = (user) => { 23 | const token = jwt.sign( 24 | { id: user.id, username: user.username }, 25 | process.env.JWT_SECRET 26 | ); 27 | return token; 28 | }; 29 | ``` 30 | 31 | This function will take a user and create a JWT from the user's id and username. This is helpful for later when we check for a JWT, we then will know what user is making the request. 32 |
33 | To do that check, we'll create custom middleware. 34 | 35 | ```ts 36 | export const protect = (req, res, next) => { 37 | const bearer = req.headers.authorization; 38 | 39 | if (!bearer) { 40 | res.status(401); 41 | res.send("Not authorized"); 42 | return; 43 | } 44 | 45 | const [, token] = bearer.split(" "); 46 | if (!token) { 47 | console.log("here"); 48 | res.status(401); 49 | res.send("Not authorized"); 50 | return; 51 | } 52 | 53 | try { 54 | const payload = jwt.verify(token, process.env.JWT_SECRET); 55 | req.user = payload; 56 | console.log(payload); 57 | next(); 58 | return; 59 | } catch (e) { 60 | console.error(e); 61 | res.status(401); 62 | res.send("Not authorized"); 63 | return; 64 | } 65 | }; 66 | ``` 67 | 68 | This middleware functions checks for a JWT on the `Authorization` header of a request. It then attaches the user to the request object before moving on. If anything fails, the user is sent a 401. 69 |
70 | We need to update our `.env` file to have a `JWT_SECRET`. You don't want this secret in your code because it's needed to sign and verify tokens. You can place whatever value you want. Then we need to load in the env file into our environment. 71 |
72 | Inside of `src/index.ts`: 73 | 74 | ```ts 75 | import * as dotenv from "dotenv"; 76 | dotenv.config(); 77 | ``` 78 | 79 | This will load in our env vars into the process. 80 | 81 |
82 | Lastly, we need to add our middleware onto our API router to protect it, so inside of `src/server.ts`, import protect and add it to the chain: 83 | 84 | ```ts 85 | app.use("/api", protect, router); 86 | ``` 87 | 88 | Now any API call to anthing `/api` will need to have a JWT. 89 |
90 | Next we'll create some routes and handlers to create users that are issued JWTs. 91 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | 4 | import { getLessons } from "../data/lesson"; 5 | 6 | import Corner from "../components/corner"; 7 | import getCourseConfig from "../data/course"; 8 | 9 | export default function Lessons({ sections }) { 10 | const courseInfo = getCourseConfig(); 11 | return ( 12 | <> 13 | 14 | {courseInfo.title} 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |

{courseInfo.title}

30 |

{courseInfo.subtitle}

31 |
32 |
33 | author image 38 |
39 |
40 |
{courseInfo.author.name}
41 |
{courseInfo.author.company}
42 |
43 |
44 |
45 |
46 |
47 | course icon 51 |
52 |
53 | {courseInfo.frontendMastersLink ? ( 54 | 55 | Watch on Frontend Masters 56 | 57 | ) : null} 58 |
59 |

Table of Contents

60 |
61 |
    62 | {sections.map((section) => ( 63 |
  1. 64 |
    65 |
    66 | 67 |
    68 |
    69 |

    {section.title}

    70 |
      71 | {section.lessons.map((lesson) => ( 72 |
    1. 73 | {lesson.title} 74 |
    2. 75 | ))} 76 |
    77 |
    78 | 79 |
    80 |
  2. 81 | ))} 82 |
83 |
84 |
85 |
86 | 87 | ); 88 | } 89 | 90 | export async function getStaticProps() { 91 | const sections = await getLessons(); 92 | return { 93 | props: { 94 | sections, 95 | }, 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /lessons/05-data-modeling/B-creating-models.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Creating DB models with with Primsa" 3 | --- 4 | 5 | ## Prisma Syntax 6 | 7 | Prisma has an easy to understand syntax for creating models. Its based on the GraphQL language which is based on JSON. So you'll feel right at home. I highly recommend installing the Prisma VS Code plugin. It lints and cleans up your schema file. 8 | 9 |
10 | Now, onto the models. Let's look at an example model. 11 |
12 | 13 | ```prisma 14 | model Post { 15 | // id field that is a number and automatically increments after its used 16 | id Int @id @default(autoincrement()) 17 | // timestamps 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @updatedAt 20 | // limit to 255 for indexing UTF-8 21 | title String @db.VarChar(255) 22 | // optional 23 | content String? 24 | published Boolean @default(false) 25 | // relation to another model 26 | author User @relation(fields: [authorId], references: [id]) 27 | authorId Int 28 | } 29 | ``` 30 | 31 | Most of this is self explanatory, but check out the comments in the code to learn a bit more context. This isn't a prisma course, so we're going to keep moving along on our API. The rest of the modeling looks very much like this. 32 | 33 | ## User 34 | 35 | ```prisma 36 | model User { 37 | id String @id @default(uuid()) 38 | createdAt DateTime @default(now()) 39 | username String @unique 40 | password String 41 | updates Update[] 42 | } 43 | ``` 44 | 45 | Above is our User schema 46 | 47 | ## Product 48 | 49 | ```prisma 50 | model Product { 51 | id String @id @default(uuid()) 52 | createdAt DateTime @default(now()) 53 | name String 54 | belongsTo User @relation(fields: [belongsToId], references: [id]) 55 | belongsToId String 56 | updates Update[] 57 | } 58 | ``` 59 | 60 | Here we have a Product schema. For the change log app, the user might have many products they want to update. So we need a place to store multiple updates. So `products` belong to a `User`. 61 | 62 | ## Update 63 | 64 | ```prisma 65 | enum UPDATE_STATUS { 66 | IN_PROGRESS 67 | LIVE 68 | DEPRECATED 69 | ARCHIVED 70 | } 71 | 72 | model Update { 73 | id String @id @default(uuid()) 74 | createdAt DateTime @default(now()) 75 | updatedAt DateTime 76 | 77 | title String @db.VarChar(255) 78 | body String 79 | status UPDATE_STATUS @default(IN_PROGRESS) 80 | version String? 81 | asset String 82 | 83 | productId String 84 | product Product @relation(fields: [productId], references: [id]) 85 | updatePoints UpdatePoint[] 86 | } 87 | ``` 88 | 89 | Products can have updates. So products belong to updates. Updates have many fields, one is called status. Because status is a finite set of options, we created an ENUM to represent our status. Think of an enum value types as "one-of-these". So the value must be one of the values in the ENUM instead of being any other random string. 90 | 91 | ## Update Points 92 | 93 | ```prisma 94 | model UpdatePoint { 95 | id String @id @default(uuid()) 96 | createdAt DateTime @default(now()) 97 | updatedAt DateTime 98 | 99 | name String @db.VarChar(255) 100 | description String 101 | 102 | updateId String 103 | update Update @relation(fields: [updateId], references: [id]) 104 | } 105 | ``` 106 | 107 | And finally, update points are the bullets points on an update. They belong to an update, which belongs to a product, which belongs to a user. 108 | 109 |
110 | As we continue to build, we will most likely make changes to our schema to fit the experience we want to create. 111 | -------------------------------------------------------------------------------- /lessons/06-routes-and-middleware/A-creating-routes.md: -------------------------------------------------------------------------------- 1 | Now that we have our schema and data models, we can start creating routes and route handlers to interact with those models. We'll be following a mostly RESTful approach to our API design, but as you'll soon learn, nothing ever stays the course when it comes to REST. 2 | 3 | ## Thinking of Routes 4 | 5 | Until some other need presents itself, we want to create a route for every CRUD action for every resource. So, in the case of a Product, we want to create: 6 | 7 | - `GET product/:id` - get a product by a given ID 8 | - `GET product` - get all the products (for an authenticated user) 9 | - `POST product` - create a new product 10 | - `PUT product/:id` - update or replace a product that matches a given ID 11 | - `DELETE product/:id` - delete a product by a give ID 12 | 13 | This is how REST looks. However, when developing an API that's consumed only by a client that you and your team also created, using something like REST is probably redundant and tedious. There's nothing stopping you from just creating an API route to get all the data for every page, or every component, or whatever makes sense for your application. Something like REST is great for external APIs so external developers can onboard more quickly because they know what to expect vs. having to learn some custom API design. 14 | 15 | ## Create our routes 16 | 17 | Create a new file, `src/router.ts` and work in there. 18 | 19 | ```ts 20 | import { Router } from "express"; 21 | 22 | const router = Router(); 23 | /** 24 | * Product 25 | */ 26 | router.get("/product", (req, res) => { 27 | res.json({ message: "product" }); 28 | }); 29 | 30 | router.get("/product/:id", (req, res) => {}); 31 | 32 | router.post("/product", (req, res) => {}); 33 | 34 | router.put("/product/:id", (req, res) => {}); 35 | 36 | router.delete("/product/:id", (req, res) => {}); 37 | 38 | /** 39 | * Update 40 | */ 41 | 42 | router.get("/update", (req, res) => {}); 43 | 44 | router.get("/update/:id", (req, res) => {}); 45 | 46 | router.post("/update", (req, res) => {}); 47 | 48 | router.put("/update/:id", (req, res) => {}); 49 | 50 | router.delete("/update/:id", (req, res) => {}); 51 | 52 | /** 53 | * UpdatePoint 54 | */ 55 | 56 | router.get("/updatepoint", (req, res) => {}); 57 | 58 | router.get("/updatepoint/:id", (req, res) => {}); 59 | 60 | router.post("/updatepoint", (req, res) => {}); 61 | 62 | router.put("/updatepoint/:id", (req, res) => {}); 63 | 64 | router.delete("/updatepoint/:id", (req, res) => {}); 65 | 66 | export default router; 67 | ``` 68 | 69 | There are a few things going on here. First we created a new router using Express. This gives us more flexibility around configuring a set of routes vs. the whole API. You can create as many routers as you'd like with Express and mount them back to the main Express app on the appropriate paths. 70 |
71 | We then created all the routes for the DB resources we want to interact with. User is noticiably missing. This is because User will have a special set of routes because of the importantance of that resource. For the handlers, we adding placeholder functions for now. If you try to make an API call, your API will get back a `404` status code and some HTML (default 404 response from Express). That's because we didn't mount this router back to the main Express app. So it's just floating and not actually attached to our API. 72 | Let's do that next: 73 |
74 | head over to `src/server.ts`: 75 | 76 | ```ts 77 | import router from './router' 78 | 79 | ....... 80 | app.use('/api', router) 81 | ``` 82 | 83 | Import the router from the other file and remove any current route declerations in `server.ts`. We then use something new here: `app.use()`, this allows you to apply a router or middleware (we will learn about middleware later) to the entire API, or in our case, to anything using the path `/api`. So a route we create in the router like `GET /product`, is now actually `GET /api/product` because the router is mounted on the `/api` path. 84 |
85 | You should now be able to hit your API and not get a 404, but, it still won't work. What's happening now is your API is hanging, which just means it never responded back to the request and there is no more code to execute. The client will eventually timeout and close the connection. This happens because we never send a response in any of the handler functions we created. We'll do that soon, but for now, lets talk about middleware. 86 | -------------------------------------------------------------------------------- /data/lesson.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import matter from "gray-matter"; 4 | import { titleCase } from "title-case"; 5 | import { marked } from "marked"; 6 | import hljs from "highlight.js"; 7 | 8 | const DEFAULT_ICON = "info-circle"; 9 | const lessonsPath = path.join(process.cwd(), "lessons"); 10 | 11 | function getTitle(slug, override) { 12 | let title = override; 13 | if (!title) { 14 | title = titleCase(slug.split("-").join(" ")); 15 | } 16 | 17 | return title; 18 | } 19 | 20 | async function getMeta(section) { 21 | let meta = {}; 22 | try { 23 | const file = await fs.readFile( 24 | path.join(lessonsPath, section, "meta.json") 25 | ); 26 | meta = JSON.parse(file.toString()); 27 | } catch (e) { 28 | // no meta.json, nothing to do 29 | } 30 | 31 | return meta; 32 | } 33 | 34 | function slugify(inputPath) { 35 | const pathParts = inputPath.split("-"); 36 | const pathOrder = pathParts.shift(); 37 | const pathSlug = pathParts.join("-"); 38 | return { 39 | slug: pathSlug, 40 | order: pathOrder, 41 | title: titleCase(pathParts.join(" ")), 42 | }; 43 | } 44 | 45 | export async function getLessons() { 46 | marked.setOptions({ 47 | baseUrl: process.env.BASE_URL ? process.env.BASE_URL + "/" : "/", 48 | highlight: function (code, lang) { 49 | const language = hljs.getLanguage(lang) ? lang : "plaintext"; 50 | return hljs.highlight(code, { language }).value; 51 | }, 52 | langPrefix: "hljs language-", 53 | }); 54 | 55 | const dir = await fs.readdir(lessonsPath); 56 | const sections = []; 57 | for (let dirFilename of dir) { 58 | const dirStats = await fs.lstat(path.join(lessonsPath, dirFilename)); 59 | if (dirStats.isFile()) { 60 | continue; 61 | } 62 | 63 | const lessonsDir = await fs.readdir(path.join(lessonsPath, dirFilename)); 64 | 65 | let { 66 | title: sectionTitle, 67 | order: sectionOrder, 68 | slug: sectionSlug, 69 | } = slugify(dirFilename); 70 | 71 | let icon = DEFAULT_ICON; 72 | 73 | const meta = await getMeta(dirFilename); 74 | if (meta.title) { 75 | sectionTitle = meta.title; 76 | } 77 | if (meta.icon) { 78 | icon = meta.icon; 79 | } 80 | 81 | const lessons = []; 82 | for (let lessonFilename of lessonsDir) { 83 | if (lessonFilename.slice(-3) !== ".md") { 84 | continue; 85 | } 86 | 87 | const filePath = path.join(lessonsPath, dirFilename, lessonFilename); 88 | 89 | const file = await fs.readFile(filePath); 90 | const { data } = matter(file.toString()); 91 | let slug = lessonFilename.replace(/\.md$/, ""); 92 | 93 | const slugParts = slug.split("-"); 94 | const lessonOrder = slugParts.shift(); 95 | 96 | slug = slugParts.join("-"); 97 | 98 | const title = getTitle(slug, data.title); 99 | 100 | lessons.push({ 101 | slug, 102 | fullSlug: `/lessons/${sectionSlug}/${slug}`, 103 | title, 104 | order: `${sectionOrder}${lessonOrder.toUpperCase()}`, 105 | path: filePath, 106 | description: data.description ? data.description : "", 107 | }); 108 | } 109 | 110 | sections.push({ 111 | icon, 112 | title: sectionTitle, 113 | slug: sectionSlug, 114 | lessons, 115 | order: sectionOrder, 116 | }); 117 | } 118 | 119 | return sections; 120 | } 121 | 122 | export async function getLesson(targetDir, targetFile) { 123 | const dir = await fs.readdir(lessonsPath); 124 | 125 | for (let i = 0; i < dir.length; i++) { 126 | const dirPath = dir[i]; 127 | if (dirPath.endsWith(targetDir)) { 128 | const lessonDir = ( 129 | await fs.readdir(path.join(lessonsPath, dirPath)) 130 | ).filter((str) => str.endsWith(".md")); 131 | 132 | for (let j = 0; j < lessonDir.length; j++) { 133 | const slugPath = lessonDir[j]; 134 | if (slugPath.endsWith(targetFile + ".md")) { 135 | const filePath = path.join(lessonsPath, dirPath, slugPath); 136 | const file = await fs.readFile(filePath); 137 | const { data, content } = matter(file.toString()); 138 | const html = marked(content); 139 | const title = getTitle(targetFile, data.title); 140 | const meta = await getMeta(dirPath); 141 | 142 | const section = getTitle(targetDir, meta.title); 143 | const icon = meta.icon ? meta.icon : DEFAULT_ICON; 144 | 145 | let nextSlug; 146 | let prevSlug; 147 | 148 | // get next 149 | if (lessonDir[j + 1]) { 150 | // has next in section 151 | 152 | const { slug: next } = slugify(lessonDir[j + 1]); 153 | nextSlug = `${targetDir}/${next.replace(/\.md$/, "")}`; 154 | } else if (dir[i + 1]) { 155 | // has next in next section 156 | const nextDir = ( 157 | await fs.readdir(path.join(lessonsPath, dir[i + 1])) 158 | ).filter((str) => str.endsWith(".md")); 159 | const nextDirSlug = slugify(dir[i + 1]).slug; 160 | const nextLessonSlug = slugify(nextDir[0]).slug.replace( 161 | /\.md$/, 162 | "" 163 | ); 164 | nextSlug = `${nextDirSlug}/${nextLessonSlug}`; 165 | } else { 166 | // last section 167 | nextSlug = null; 168 | } 169 | 170 | // get prev 171 | if (lessonDir[j - 1]) { 172 | // has prev in section 173 | const { slug: prev } = slugify(lessonDir[j - 1]); 174 | prevSlug = `${targetDir}/${prev.replace(/\.md$/, "")}`; 175 | } else if (dir[i - 1]) { 176 | // has prev in prev section 177 | const prevDir = ( 178 | await fs.readdir(path.join(lessonsPath, dir[i - 1])) 179 | ).filter((str) => str.endsWith(".md")); 180 | const prevDirSlug = slugify(dir[i - 1]).slug; 181 | const prevLessonSlug = slugify( 182 | prevDir[prevDir.length - 1] 183 | ).slug.replace(/\.md$/, ""); 184 | prevSlug = `${prevDirSlug}/${prevLessonSlug}`; 185 | } else { 186 | // first section 187 | prevSlug = null; 188 | } 189 | 190 | const base = process.env.BASE_URL ? process.env.BASE_URL : "/"; 191 | 192 | return { 193 | attributes: data, 194 | html, 195 | slug: targetFile, 196 | title, 197 | section, 198 | icon, 199 | filePath, 200 | nextSlug: nextSlug ? path.join(base, "lessons", nextSlug) : null, 201 | prevSlug: prevSlug ? path.join(base, "lessons", prevSlug) : null, 202 | }; 203 | } 204 | } 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

next-course-starter


2 | 3 |

4 | A NextJS starter to get you started creating educational materials using Markdown 5 |

6 | 7 | ## Get Started 8 | 9 | 1. Set up Node.js v14+ 10 | 1. Clone this repo 11 | 1. Run `npm install` 12 | 1. Run `npm run dev` to start the dev server 13 | 1. Open http://localhost:3000 in a browser 14 | 15 | ## Configure Your Course 16 | 17 | There are several things to configure before getting started. 18 | 19 | ### course.json 20 | 21 | This json file allows you to configure the details of the site. Update the info here and it'll update it everywhere throughout the course website. 22 | 23 | - _author.name_ – Your name 24 | - _author.company_ – The company you work at or whatever you want as your subtitle. Optional. 25 | - _title_ – The title of your course 26 | - _subtitle_ – The subtitle of your course. Optional. 27 | - _frontendMastersLink_ – A link to the published video on FrontendMasters.com. Optional. 28 | - _social.linkedin_ - Your LinkedIn public user name, just the name, not the full link. Optional 29 | - _social.twitter_ – Your Twitter user name. Optional. 30 | - _social.github_ – Your GitHub user name. Optional. 31 | - _description_ – The description you want to show up in search engine results. 32 | - _keywords_ – The SEO keywords for this course. An array of strings 33 | - _productionBaseUrl_ – Typically useful for GitHub Pages. This adds a base path to your project. For GitHub Pages, this will be the name of your repo. For example, this site's base URL is `/next-course-starter` because the production URL for this site is `btholt.github.io/next-course-starer`. Do note this will also make your dev server's base URL this as well so you can catch problems before they go to production. 34 | - _csvPath_ – A CSV with the meta data for your lessons will be created at this path when you build or export the project. If you delete this config option it will not generate a CSV. 35 | 36 | ### styles/variables.css 37 | 38 | Here is where you can theme your site. You can retheme the whole site with just these. 39 | 40 | ### public/images 41 | 42 | Here is where you should stick all your images. Inside of your markdown, refer to images in this folder as `./images/`. 43 | 44 | Note this site doesn't use `next/image` because that requires the server component. 45 | 46 | ### public/images/author.jpg 47 | 48 | Your image. If you call it this, you won't have to change any code. If you need to change it, it's in `pages/index.js`. 49 | 50 | ### public/images/social-share-cover.jpg 51 | 52 | The image that will be used if someone shares your website to Twitter/Facebook/etc. If you call it this, you won't have to change any code. If you do need to change it, it's in `pages/index.js` 53 | 54 | ### public/images/course-icon.png 55 | 56 | The image at the top of the course. If you call it this, you won't have to change any code. If you do need to change it, it's in `pages/index.js` 57 | 58 | ## Favicon 59 | 60 | Just replace the favicon\* files and the apple-touch-icon.png in the public/images directory. If you have a PNG, [favicon.io](https://favicon.io) will generate these files for you. If you don't want favicons, just remove the references to them in `pages/_app.js`. 61 | 62 | ## Lessons 63 | 64 | All your markdown lesson files will go in `lessons/`. They **must** be organized an named this way: 65 | 66 | The folders must be named `01-section-one-name`, `02-section-two-name`, `03-section-three`, etc. 67 | 68 | The lessons are then grouped inside of these, the lessons are ordered by letters, `A-lesson-one.md`, `B-lesson-two.md`, `C-lesson-three.md`, etc. This numbering scheme matches how Frontend Masters organizes their content. 69 | 70 | The titles of your lessons and sections are generated from the folder and lesson names (and can be overridden.) The first, organizing part of the name is stripped (the `01-` part of `01-section-one` and the `A-` part of `A-lesson-one`), the hyphens are turned into spaces (`section-one` becomes `section one`) and then those are run through [title-case](https://github.com/blakeembrey/change-case#titlecase) (so `section one` becomes `Section One`.) If you need to override these, use the frontmatter (explained below.) 71 | 72 | The folder and lesson names are also used for the slugs. `02-section-two/C-lesson-three.md` becomes `yoursite.com/lessons/section-two/lesson-three`. 73 | 74 | Each of these lessons can have a [frontmatter](https://github.com/jonschlinkert/gray-matter#readme) for the following properties 75 | 76 | - _title_ – If you want the title to be different from the file name, you can specify here what that title should be. Frequently this useful for things where the capitalization would be off e.g. TailwindCSS instead of Tailwindcss. Optional. 77 | - _description_ – If you want to give individual lessons descriptions for SEO and for Frontend Masters, you can write a brief description here. 78 | 79 | Be aware because of how the numbers and letters are stripped out, it is possible to have ambigious paths. `01-welcome/A-intro.md` and `03-welcome/D-intro.md` would resolve to the same thing and only the first one would be visitable. 80 | 81 | ## meta.json 82 | 83 | Each **section** (e.g. inside of `03-section-three` folder) folder can have a meta.json file, and is totally optional. 84 | 85 | - _title_ – an override for the title of the section. Frequently useful for capitalization e.g. `JS Tools` instead of `Js Tools`. 86 | - _icon_ – so you can set the icon used in the home page and the header. These icons are pulled from [the free Font Awesome v5 icons](https://fontawesome.com/v5.15/icons). If you want [fa-hammer](https://fontawesome.com/v5.15/icons/hammer), use "hammer" as the value for icon. 87 | 88 | ## highlight.js Theme 89 | 90 | The code blocks use [Highlight.js](https://highlightjs.org/static/demo/). By default it will use `a11y-light` as the theme for the code blocks. Change the CSS import in `pages/_app.js` to the theme you want to use. 91 | 92 | ## GitHub Pages / GitHub Actions 93 | 94 | By default this repo works with GitHub Pages. Just make sure you set the `productionBaseUrl` in the course.json to the name of the repo. 95 | 96 | It also includes a GitHub Action to automatically deploy to your gh-pages branch. Just make sure that your repo has GitHub Pages enabled and the branch is set to gh-pages. If you're not deploying to GitHub Pages go ahead and delete the `.github` directory. 97 | 98 | By default the GitHub Action looks for a `main` branch, so be sure you're using that instead of `master`. 99 | 100 | If you want a custom domain, make sure you uncomment the `fqdn` field in [.github/workflows/next.yaml](https://github.com/btholt/next-course-starter/blob/main/.github/workflows/next.yaml) file and put your custom domain. If you don't do that and only set it up with the GitHub web GUI, every deploy will break the custom domain. 101 | 102 | ## Example Sites 103 | 104 | - [This repo itself](https://btholt.github.io/next-course-starter/) 105 | - [Complete Intro to React v6](https://btholt.github.io/next-react-v6/) 106 | - [Complete Intro to React v7](https://btholt.github.io/complete-intro-to-react-v7/) 107 | - [Complete Intro to Web Dev v3](https://btholt.github.io/complete-intro-to-web-dev-v3/) 108 | 109 | ## npm commands 110 | 111 | - `npm run dev` - Next's dev command. Start a local dev server. Note if you have a productionBasePath set in your course.json, your dev server will respect that (so you don't mess up your paths in production.) 112 | - `npm run build` - Build your site for production. This will still include the Next.js server run time. Use this if you're using something like Vercel to host your site. 113 | - `npm run export` - Builds your site statically, use this if you're going to deploy to GitHub Pages, S3, or somewhere else with no server. This will run next build and then next export (no need to run build yourself first.) 114 | - `npm run start` - Start an already-built server. 115 | - `npm run csv` – Will generate the CSV of the metadata from your course. Note you may have to run build first, depending on your csvPath. 116 | 117 | ## Analytics 118 | 119 | By default this doesn't generate any analytics. If you are creating a Frontend Masters course and would like a weekly report of analytics, contact me (Brian Holt) and I'll give you a snippet to drop on your page (though fair warning, I will also have access to your data, if that bothers you.) 120 | 121 | Otherwise I'm pretty pleased with [Simple Analytics](referral.simpleanalytics.com/brian) (this is a referral link, free month for me and free month for you); I've been using them for my courses personally. 122 | 123 | ## License 124 | 125 | The **code** is this repo is licensed under the Apache 2.0 license. 126 | 127 | I include the CC-BY-NC-4.0 license for the content; this is what I recommend you license your **content** under: anyone can use and share the content but they cannot sell it; only you can. 128 | -------------------------------------------------------------------------------- /styles/courses.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | 3 | /* mini css reset */ 4 | html { 5 | font-size: 16px; 6 | } 7 | 8 | code { 9 | background-color: rgba(255, 255, 255, 0.2); 10 | padding: 4px; 11 | color: salmon; 12 | /* font-weight: bold; */ 13 | } 14 | 15 | body, 16 | h1, 17 | h2, 18 | h3, 19 | h4, 20 | h5, 21 | h6, 22 | p, 23 | ol, 24 | ul { 25 | margin: 0; 26 | padding: 0; 27 | font-weight: normal; 28 | } 29 | 30 | h2, h3 { 31 | margin: 1rem 0; 32 | } 33 | 34 | img { 35 | max-width: 100%; 36 | height: auto; 37 | } 38 | 39 | * { 40 | box-sizing: border-box; 41 | } 42 | 43 | body { 44 | font-family: "Open Sans"; 45 | background: linear-gradient(90deg, var(--bg-main) 15px, transparent 1%) center, 46 | linear-gradient(var(--bg-main) 15px, transparent 1%) center, var(--bg-dots); 47 | background-size: 16px 16px; 48 | margin: 0; 49 | } 50 | 51 | a { 52 | color: var(--text-links); 53 | text-decoration: none; 54 | } 55 | 56 | .navbar { 57 | border-bottom: 1px solid #ccc; 58 | position: fixed; 59 | width: 100%; 60 | top: 0; 61 | z-index: 10; 62 | display: flex; 63 | justify-content: space-between; 64 | align-items: center; 65 | background-color: var(--bg-main); 66 | padding: 10px; 67 | } 68 | 69 | .navbar h1 { 70 | font-size: 20px; 71 | margin: inherit; 72 | padding: inherit; 73 | font-weight: bold; 74 | color: var(--text-main); 75 | } 76 | 77 | .navbar h2 { 78 | font-size: 14px; 79 | margin: inherit; 80 | margin-left: 15px; 81 | padding: inherit; 82 | text-transform: uppercase; 83 | color: var(--text-main-headers); 84 | } 85 | 86 | .navbar-info { 87 | display: flex; 88 | flex-direction: row; 89 | align-items: center; 90 | justify-content: center; 91 | } 92 | 93 | header .cta-btn { 94 | display: none; /* only displays at large screen sizes */ 95 | } 96 | 97 | .main .cta-btn { 98 | width: 90%; 99 | margin: 20px auto 0px auto; 100 | max-width: 500px; 101 | padding: 12px 20px; 102 | } 103 | 104 | .cta-btn { 105 | border-radius: 10px; 106 | background: var(--nav-buttons); 107 | color: var(--nav-buttons-text); 108 | padding: 7px 20px; 109 | display: flex; 110 | justify-content: center; 111 | align-items: center; 112 | } 113 | 114 | .jumbotron { 115 | padding: 0; 116 | } 117 | 118 | .jumbotron .courseInfo, 119 | .jumbotron .courseIcon { 120 | padding: 20px; 121 | } 122 | 123 | .jumbotron .courseInfo, 124 | .jumbotron .courseIcon { 125 | text-align: center; 126 | } 127 | 128 | .author { 129 | margin-top: 40px; 130 | display: flex; 131 | justify-content: center; 132 | } 133 | 134 | @media (min-width: 1000px) { 135 | header .cta-btn { 136 | display: flex; 137 | } 138 | 139 | .main .cta-btn { 140 | display: none; 141 | } 142 | 143 | .jumbotron { 144 | display: flex; 145 | width: 100%; 146 | min-height: 45vh; 147 | } 148 | .jumbotron .courseInfo, 149 | .jumbotron .courseIcon { 150 | display: flex; 151 | justify-content: center; 152 | align-items: center; 153 | } 154 | .jumbotron .courseInfo { 155 | width: 65%; 156 | text-align: right; 157 | } 158 | .jumbotron .courseIcon { 159 | width: 35%; 160 | display: flex; 161 | align-items: center; 162 | justify-content: center; 163 | } 164 | 165 | .author { 166 | justify-content: flex-end; 167 | } 168 | .jumbotron .courseInfo-inner { 169 | max-width: 85%; 170 | } 171 | } 172 | 173 | .jumbotron h1, 174 | .jumbotron h2 { 175 | color: var(--text-main-headers); 176 | } 177 | 178 | .jumbotron h1 { 179 | font-size: 50px; 180 | margin-bottom: 20px; 181 | } 182 | 183 | .jumbotron .courseInfo { 184 | background: var(--primary); 185 | } 186 | 187 | .jumbotron .courseIcon { 188 | background: var(--secondary); 189 | } 190 | 191 | .jumbotron .courseIcon img { 192 | max-width: 180px; 193 | } 194 | 195 | .author .info { 196 | padding: 10px; 197 | } 198 | 199 | .author .name { 200 | font-size: 18px; 201 | font-weight: bold; 202 | color: var(--text-main-headers); 203 | } 204 | 205 | .author .company { 206 | color: var(--text-main-headers); 207 | font-size: 16px; 208 | } 209 | 210 | .author .image { 211 | border-radius: 75px; 212 | overflow: hidden; 213 | height: 75px; 214 | width: 75px; 215 | } 216 | 217 | .navbar-brand.navbar-brand a { 218 | text-transform: uppercase; 219 | font-weight: bold; 220 | color: var(--text-main-headers); 221 | } 222 | 223 | .lesson-section-title { 224 | color: var(--text-main-headers); 225 | } 226 | 227 | .lesson-container { 228 | position: relative; 229 | max-width: 900px; 230 | margin: 0 auto 45px auto; 231 | padding: 10px 40px; 232 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 233 | background-color: var(--bg-lesson); 234 | border-radius: 5px; 235 | margin-top: 40px; 236 | } 237 | 238 | .lesson { 239 | margin: 15px; 240 | padding: 15px; 241 | border-radius: 8px; 242 | } 243 | 244 | .lesson > h1 { 245 | color: var(--text-header); 246 | font-size: 24px; 247 | } 248 | 249 | .lesson h2 { 250 | font-size: 24px; 251 | margin-top: 20px; 252 | margin-bottom: 10px; 253 | } 254 | 255 | .lesson h2::after { 256 | content: ""; 257 | display: block; 258 | height: 3px; 259 | margin-top: 5px; 260 | background: var(--text-header); 261 | max-width: 300px; 262 | } 263 | 264 | .lesson p { 265 | clear: both; 266 | } 267 | 268 | .lesson p, 269 | .lesson li { 270 | line-height: 180%; 271 | } 272 | 273 | .lesson-links { 274 | font-size: 18px; 275 | padding: 15px 0; 276 | } 277 | 278 | .next { 279 | float: right; 280 | } 281 | 282 | .prev { 283 | float: left; 284 | } 285 | 286 | .lesson-title { 287 | text-transform: uppercase; 288 | font-weight: bold; 289 | } 290 | 291 | .gatsby-highlight { 292 | padding: 4px; 293 | border-radius: 4px; 294 | display: flex; 295 | justify-content: space-between; 296 | flex-direction: column; 297 | align-items: stretch; 298 | } 299 | 300 | .lesson-content td { 301 | border: 1px solid black; 302 | padding: 8px; 303 | } 304 | 305 | .lesson-content td input { 306 | min-width: 300px; 307 | } 308 | 309 | .lesson-content img { 310 | margin: 5px auto; 311 | display: block; 312 | } 313 | 314 | .lesson-flex { 315 | display: flex; 316 | flex-direction: column; 317 | justify-content: center; 318 | align-items: center; 319 | } 320 | 321 | .random-tweet { 322 | width: 100%; 323 | margin-top: 90px; 324 | } 325 | 326 | .fem-link { 327 | text-align: center; 328 | } 329 | 330 | .content-container { 331 | display: flex; 332 | flex-direction: column; 333 | justify-content: space-between; 334 | min-height: 100vh; 335 | padding-top: 50px; 336 | } 337 | 338 | blockquote { 339 | padding: 15px; 340 | background-color: var(--emphasized-bg); 341 | border: 2px solid var(--emphasized-border); 342 | border-radius: 5px; 343 | width: 100%; 344 | margin: 10px 0; 345 | } 346 | 347 | blockquote > *:last-child { 348 | margin-bottom: 0; 349 | } 350 | 351 | .lesson-content img { 352 | max-width: 100%; 353 | } 354 | 355 | .main-card { 356 | max-width: 900px; 357 | margin: 0 auto; 358 | overflow: hidden; 359 | } 360 | 361 | .lesson-title { 362 | font-size: 20px; 363 | padding: 15px 30px; 364 | } 365 | 366 | .lesson-content { 367 | line-height: 1.5; 368 | color: var(--text-lesson-body); 369 | } 370 | 371 | .lesson-text { 372 | width: 100%; 373 | padding: 25px 5px 25px 35px; 374 | min-height: 200px; 375 | } 376 | 377 | .sections-name { 378 | margin: 0; 379 | padding: 0; 380 | } 381 | 382 | ol.sections-name { 383 | counter-reset: my-awesome-counter; 384 | list-style: none; 385 | padding-left: 40px; 386 | width: 98%; 387 | margin: 0; 388 | padding: 0; 389 | } 390 | 391 | ol.sections-name > li { 392 | counter-increment: my-awesome-counter; 393 | display: flex; 394 | flex-direction: row; 395 | flex-wrap: wrap; 396 | margin-bottom: 35px; 397 | width: 100%; 398 | box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2); 399 | border-bottom-right-radius: 5px; 400 | border-top-right-radius: 5px; 401 | } 402 | ol.sections-name .lesson-preface { 403 | color: var(--icons); 404 | display: flex; 405 | position: relative; 406 | align-items: center; 407 | justify-content: center; 408 | background: var(--bg-lesson-preface); 409 | font-size: 100px; 410 | padding: 25px; 411 | width: 40%; 412 | } 413 | 414 | .lesson-preface.lesson-preface > svg { 415 | width: 80%; 416 | height: inherit; 417 | max-height: 100px; 418 | } 419 | 420 | ol.sections-name .lesson-preface::before { 421 | content: counter(my-awesome-counter); 422 | position: absolute; 423 | top: 0; 424 | left: 5px; 425 | font-size: 20px; 426 | font-weight: bold; 427 | color: var(--icons); 428 | } 429 | 430 | ol.sections-name .lesson-details { 431 | display: flex; 432 | flex-basis: 100%; 433 | flex: 1; 434 | background: var(--bg-lesson); 435 | position: relative; 436 | } 437 | 438 | .details-bg { 439 | --corner-fill: var(--corner-inactive); 440 | transition: fill 0.25s; 441 | width: 10%; 442 | height: 0; 443 | padding-bottom: 10%; 444 | background-size: cover; 445 | background-repeat: no-repeat; 446 | position: absolute; 447 | top: 0; 448 | right: 0; 449 | } 450 | 451 | .details-bg > svg { 452 | width: 100%; 453 | height: auto; 454 | } 455 | 456 | .details-bg > svg path { 457 | transition: fill 0.25s; 458 | } 459 | 460 | .lesson-details:hover .details-bg, 461 | .lesson-container .details-bg { 462 | --corner-fill: var(--corner-active); 463 | } 464 | 465 | @media (min-width: 1000px) { 466 | ol.sections-name > li::before { 467 | border-bottom-left-radius: 5px; 468 | border-top-left-radius: 5px; 469 | } 470 | ol.sections-name .lesson-details { 471 | border-bottom-right-radius: 5px; 472 | border-top-right-radius: 5px; 473 | } 474 | } 475 | 476 | @media (max-width: 600px) { 477 | .lesson-container { 478 | padding: 2px; 479 | } 480 | 481 | ol.sections-name .lesson-preface { 482 | font-size: 60px; 483 | } 484 | } 485 | 486 | .lesson-details h3 { 487 | font-size: 22px; 488 | border-bottom: 1px solid var(--less); 489 | padding-bottom: 10px; 490 | display: inline-block; 491 | font-weight: bold; 492 | margin-bottom: 20px; 493 | } 494 | 495 | .lesson-links { 496 | margin-top: 45px; 497 | margin-bottom: 80px; 498 | } 499 | 500 | .lesson-links a { 501 | border-radius: 10px; 502 | background: var(--nav-buttons); 503 | color: var(--nav-buttons-text); 504 | padding: 15px 20px; 505 | display: inline-block; 506 | display: flex; 507 | justify-content: center; 508 | align-items: center; 509 | transition: background 0.2s; 510 | } 511 | 512 | .lesson-links a.prev { 513 | padding-left: 10px; 514 | } 515 | 516 | .lesson-links a.next { 517 | padding-right: 10px; 518 | } 519 | 520 | .lesson-links a:hover { 521 | background: var(--nav-buttons-hover); 522 | text-decoration: none; 523 | } 524 | 525 | .lesson-links .arrow { 526 | font-size: 24px; 527 | line-height: 24px; 528 | padding: 0px 5px; 529 | } 530 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------