├── .env.example
├── .gitignore
├── .prettierrc.json
├── .sequelizerc
├── LICENSE
├── README.md
├── components
├── Button.tsx
├── Header.tsx
└── Loader.tsx
├── db
├── index.ts
├── migrations
│ ├── 20200917162058-create-user.js
│ └── 20200917185257-create-post.js
└── models
│ ├── helpers.ts
│ ├── index.ts
│ ├── post.ts
│ └── user.ts
├── docs
├── aws-rds-example.png
├── env-vars.png
└── stack.png
├── lib
└── auth
│ ├── jwt.ts
│ └── privateRoute.tsx
├── next-env.d.ts
├── package.json
├── pages
├── _app.tsx
├── api-demo.tsx
├── api
│ ├── auth
│ │ └── [...nextauth].ts
│ ├── post
│ │ ├── [id]
│ │ │ └── delete.ts
│ │ ├── create.ts
│ │ └── index.ts
│ ├── private.ts
│ └── public.ts
├── crud.tsx
├── index.tsx
├── private.tsx
└── public.tsx
├── postcss.config.js
├── public
├── favicon.ico
└── vercel.svg
├── styles
└── index.css
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | API_URL=http://localhost:3000
2 | NEXTAUTH_URL=http://localhost:3000
3 | AUTH_SECRET=a-random-string
4 | JWT_SECRET=a-random-string
5 |
6 | DATABASE_URL=
7 | GOOGLE_CLIENT_ID=
8 | GOOGLE_CLIENT_SECRET=
9 | GITHUB_CLIENT_ID=
10 | GITHUB_CLIENT_SECRET=
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": true,
4 | "tabWidth": 2,
5 | "trailingComma": "es5",
6 | "arrowParens": "always"
7 | }
8 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | require("dotenv").config({ path: path.resolve(process.cwd(), '.env.local') });
4 |
5 | module.exports = {
6 | "url": process.env.DATABASE_URL,
7 | "models-path": path.resolve("db", "models"),
8 | "seeders-path": path.resolve("db", "seeders"),
9 | "migrations-path": path.resolve("db", "migrations")
10 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Belay Inc.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
🚲 next-fullstack
3 |
4 | A lightweight boilerplate for developing full-stack applications with Next.js
5 |
6 |
7 |
8 |
9 | **[Check out the demo 📎](https://next-fullstack-demo.vercel.app/)**
10 |
11 | This full-stack boilerplates comes with [Sequelize](https://sequelize.org/master/) (a Node.js ORM), [Tailwind CSS](https://tailwindcss.com/) (utility-first CSS framework), and basic authentication with [NextAuth.js](https://next-auth.js.org/). Minimal setup is needed to deploy a basic CRUD application.
12 |
13 |
14 |
15 | The entire boilerplate is written in [Typescript](https://www.typescriptlang.org/) and is set up with pre-commit hooks that compiles Typescript and runs [prettier](https://prettier.io/) (code formatter).
16 |
17 |
18 |
19 | ## From cloning to deploying, a step by step guide
20 | Follow along to get your own version of this boilerplate deployed on Vercel.
21 |
22 |
23 |
24 | #### Fork and clone repository
25 | [Fork the repository](https://guides.github.com/activities/forking/) into your own account then clone the fork to your local development environment.
26 | ```
27 | git clone git@github.com:[USERNAME]/next-fullstack.git
28 | ```
29 |
30 |
31 | #### Install dependencies
32 | ```
33 | yarn install
34 | ```
35 |
36 |
37 | #### Set up local environment variable
38 | The environment variables required by this boilerplate can be seen in `.env.example`. Create a local environment variable file:
39 |
40 | ```
41 | cp .env.example .env.local
42 | ```
43 |
44 | We'll be setting up a database and also OAuth providers in upcoming steps to get values for these variables.
45 |
46 |
47 |
48 | #### Create a Postgres database
49 | You can create a Postgres database with any service provider. Make sure it is publicly accessible and is password authenticated.
50 |
51 | 👉 [See example settings for creating an AWS RDS Postgres database.](docs/aws-rds-example.png)
52 |
53 | After creation, compose the database URL and update your local environment variable file (`.env.local`)
54 | ```
55 | DATABASE_URL=postgres://[USERNAME]:[PASSWORD]@[HOST]:[PORT]/postgres
56 | ```
57 |
58 |
59 | #### Run migrations
60 | Create tables `users` and `posts`.
61 |
62 | ```
63 | yarn sequelize-cli db:migrate
64 | ```
65 |
66 | These are example models and tables. Feel free to roll back (`yarn sequelize-cli db:migrate:undo`) and write your own migrations after you have the basic boilerplate up and running.
67 |
68 |
69 |
70 | #### Set up OAuth providers
71 | The boilerplate comes set up with Github and Google as OAuth providers, however you are free to [remove or add your own](https://next-auth.js.org/providers/github) by editing the provider entries in the [`[next-auth].ts` file](https://github.com/belay-labs/next-fullstack/blob/master/pages/api/auth/%5B...nextauth%5D.ts#L11) and adding the relevant environment variables.
72 |
73 | 👉 [Setting up Google OAuth](https://support.google.com/cloud/answer/6158849?hl=en)\
74 | 👉 [Setting up Github OAuth](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app)
75 |
76 |
77 |
78 | Update environment variables with your OAuth client ID and secret. e.g. for Github:
79 | ```
80 | GITHUB_CLIENT_ID=[GITHUB_CLIENT_ID]
81 | GITHUB_CLIENT_SECRET=[GITHUB_CLIENT_SECRET]
82 | ```
83 |
84 |
85 |
86 | #### Run locally
87 | ```
88 | yarn dev
89 | ```
90 | 🚀 Go to [localhost:3000](http://localhost:3000/)!
91 |
92 |
93 |
94 | #### Deploy to Vercel
95 | Applications developed from this boilerplate can be hosted anywhere. These instructions are for deploying via Vercel.
96 |
97 | 1. [Import](https://vercel.com/import) your project from Github
98 | 2. Set your environment variables - you won't know what `API_URL` and `NEXTAUTH_URL` will be until after your first deploy. Vercel will issue your project a unique domain.
99 |
100 |
101 |
102 | 3. After deployment grab the domain and update the `API_URL` and `NEXTAUTH_URL` environment variables.
103 | 4. Redeploy for new variable to take effect (you can trigger this by pushing to master).
104 |
105 |
106 |
107 | ## Contributing
108 |
109 | **[🐛 Submit a bug](https://github.com/belay-labs/next-fullstack/issues/new?labels=bug&template=bug_report.md)** | **[🐥 Submit a feature request](https://github.com/belay-labs/next-fullstack/issues/new?labels=feature-request&template=feature_request.md)**
110 |
111 | #### Review & deployment
112 |
113 | Create a PR describing the change you've made and someone will be along to review it and get it merged to master. After changes are merged to `master`, we'll trigger a production deployment to https://next-fullstack-demo.vercel.app/.
114 |
115 |
116 |
117 | ## Maintainers
118 | Hi! We're [Cathy](https://github.com/cathykc), [Stedman](https://github.com/stedmanblake), and [Zain](https://github.com/tarzain). Feel free email us at cathy@belaylabs.com! 👋
119 |
120 |
121 |
122 | ## License
123 | [](https://opensource.org/licenses/Apache-2.0)
124 |
125 |
126 | This project is licensed under the terms of the [Apache-2.0](LICENSE).
127 |
--------------------------------------------------------------------------------
/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface Props {
4 | className?: string;
5 | disabled?: boolean;
6 | hoverClassName?: string;
7 | iconPath: JSX.Element;
8 | loading?: boolean;
9 | onClick: () => void;
10 | text: string;
11 | }
12 |
13 | const Button = ({
14 | className,
15 | disabled,
16 | hoverClassName,
17 | iconPath,
18 | loading,
19 | onClick,
20 | text,
21 | }: Props) => {
22 | const normalStyle = className ? className : "bg-purple-500 text-white";
23 | const hoverStyle = hoverClassName ? hoverClassName : "hover:bg-purple-700";
24 | const disabledStyle = "opacity-50 cursor-not-allowed";
25 |
26 | const handleClick = () => {
27 | if (disabled || loading) return;
28 | onClick();
29 | };
30 |
31 | return (
32 |
61 | );
62 | };
63 |
64 | export default Button;
65 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { signIn, signOut, useSession } from "next-auth/client";
2 | import Link from "next/link";
3 | import { useRouter } from "next/router";
4 |
5 | interface Props {
6 | session: any;
7 | }
8 |
9 | interface ButtonProps {
10 | active?: boolean;
11 | onClick?: () => void;
12 | text: string;
13 | }
14 |
15 | const HeaderButton = ({ active, onClick, text }: ButtonProps) => {
16 | return (
17 |
25 | );
26 | };
27 |
28 | const Header = () => {
29 | const router = useRouter();
30 | const [session, loading] = useSession();
31 |
32 | return (
33 |
10 | You are {session ? "signed in" : "not signed in"}.
11 |
12 |
13 |
14 |
Public API endpoint
15 |
19 |
20 |
21 |
22 | Private API endpoint (sign in to see response)
23 |
24 |
28 |
29 | >
30 | );
31 | };
32 |
33 | export default ApiDemo;
34 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import NextAuth, { InitOptions } from "next-auth";
3 | import Providers from "next-auth/providers";
4 |
5 | import db from "../../../db";
6 |
7 | // For more information on each option (and a full list of options) go to
8 | // https://next-auth.js.org/configuration/options
9 | const options: InitOptions = {
10 | // https://next-auth.js.org/configuration/providers
11 | providers: [
12 | Providers.Google({
13 | clientId: process.env.GOOGLE_CLIENT_ID!,
14 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
15 | }),
16 | Providers.GitHub({
17 | clientId: process.env.GITHUB_CLIENT_ID!,
18 | clientSecret: process.env.GITHUB_CLIENT_SECRET!,
19 | }),
20 | ],
21 | // Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
22 | // https://next-auth.js.org/configuration/database
23 | //
24 | // Notes:
25 | // * You must to install an appropriate node_module for your database
26 | // * The Email provider requires a database (OAuth providers do not)
27 | // database: process.env.DATABASE_URL,
28 |
29 | // The secret should be set to a reasonably long random string.
30 | // It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
31 | // a seperate secret is defined explicitly for encrypting the JWT.
32 | secret: process.env.AUTH_SECRET,
33 |
34 | session: {
35 | // Use JSON Web Tokens for session instead of database sessions.
36 | // This option can be used with or without a database for users/accounts.
37 | // Note: `jwt` is automatically set to `true` if no database is specified.
38 | jwt: true,
39 |
40 | // Seconds - How long until an idle session expires and is no longer valid.
41 | // maxAge: 30 * 24 * 60 * 60, // 30 days
42 |
43 | // Seconds - Throttle how frequently to write to database to extend a session.
44 | // Use it to limit write operations. Set to 0 to always update the database.
45 | // Note: This option is ignored if using JSON Web Tokens
46 | // updateAge: 24 * 60 * 60, // 24 hours
47 | },
48 |
49 | // JSON Web tokens are only used for sessions if the `jwt: true` session
50 | // option is set - or by default if no database is specified.
51 | // https://next-auth.js.org/configuration/options#jwt
52 | jwt: {
53 | // A secret to use for key generation (you should set this explicitly)
54 | secret: process.env.JWT_SECRET!,
55 | // Set to true to use encryption (default: false)
56 | // encryption: true,
57 | // You can define your own encode/decode functions for signing and encryption
58 | // if you want to override the default behaviour.
59 | // encode: async ({ secret, token, maxAge }) => {},
60 | // decode: async ({ secret, token, maxAge }) => {},
61 | },
62 |
63 | // You can define custom pages to override the built-in pages.
64 | // The routes shown here are the default URLs that will be used when a custom
65 | // pages is not specified for that route.
66 | // https://next-auth.js.org/configuration/pages
67 | pages: {
68 | // signIn: '/api/auth/signin', // Displays signin buttons
69 | // signOut: '/api/auth/signout', // Displays form with sign out button
70 | // error: '/api/auth/error', // Error code passed in query string as ?error=
71 | // verifyRequest: '/api/auth/verify-request', // Used for check email page
72 | // newUser: null // If set, new users will be directed here on first sign in
73 | },
74 |
75 | // Callbacks are asynchronous functions you can use to control what happens
76 | // when an action is performed.
77 | // https://next-auth.js.org/configuration/callbacks
78 | callbacks: {
79 | // signIn: async (user, account, profile) => { return Promise.resolve(true) },
80 | // redirect: async (url, baseUrl) => { return Promise.resolve(baseUrl) },
81 | // session: async (session, user) => { return Promise.resolve(session) },
82 | jwt: async (
83 | token: any,
84 | user: any,
85 | account: any,
86 | profile: any,
87 | isNewUser: boolean
88 | ) => {
89 | // Save user to database
90 | if (user) {
91 | const { email, image, name } = user;
92 |
93 | const userAttrs = {
94 | email,
95 | imgUrl: image,
96 | name,
97 | };
98 |
99 | const [dbUser, userCreated] = await db.User.findOrCreate({
100 | where: { email },
101 | defaults: userAttrs,
102 | });
103 |
104 | // Update user with new information from oauth if entry already existed
105 | if (!userCreated) await dbUser.update(userAttrs);
106 |
107 | // Add user ID to token
108 | token.userId = dbUser.id;
109 | }
110 |
111 | return Promise.resolve(token);
112 | },
113 | },
114 |
115 | // Events are useful for logging
116 | // https://next-auth.js.org/configuration/events
117 | events: {},
118 |
119 | // Enable debug messages in the console if you are having problems
120 | debug: false,
121 | };
122 |
123 | export default (req: NextApiRequest, res: NextApiResponse) =>
124 | NextAuth(req, res, options);
125 |
--------------------------------------------------------------------------------
/pages/api/post/[id]/delete.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import db from "../../../../db";
4 | import { requestWrapper } from "../../../../lib/auth/jwt";
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | const { text } = req.body;
8 |
9 | await requestWrapper(req, res, async (token: any) => {
10 | const { query } = req;
11 |
12 | await db.Post.destroy({
13 | where: { id: query.id },
14 | });
15 |
16 | res.status(200).end();
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/pages/api/post/create.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import db from "../../../db";
4 | import { requestWrapper } from "../../../lib/auth/jwt";
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | const { text } = req.body;
8 |
9 | await requestWrapper(req, res, async (token: any) => {
10 | const post = await db.Post.create({
11 | text,
12 | createdById: token.userId,
13 | });
14 |
15 | res.status(200).json(post);
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/pages/api/post/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import db from "../../../db";
4 | import { requestWrapper } from "../../../lib/auth/jwt";
5 |
6 | export default async (req: NextApiRequest, res: NextApiResponse) => {
7 | await requestWrapper(req, res, async (token: any) => {
8 | const posts = await db.Post.findAll({
9 | include: { model: db.User, as: "createdBy" },
10 | order: [["createdAt", "DESC"]],
11 | });
12 |
13 | res.status(200).json({ posts });
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/pages/api/private.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { requestWrapper } from "../../lib/auth/jwt";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | await requestWrapper(req, res, (token: any) => {
7 | res.status(200).json({ data: ["foo", "bar"] });
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/pages/api/public.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { requestWrapper } from "../../lib/auth/jwt";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | res.status(200).json({ data: ["foo", "bar"] });
7 | };
8 |
--------------------------------------------------------------------------------
/pages/crud.tsx:
--------------------------------------------------------------------------------
1 | import { concat, filter, map } from "lodash";
2 | import { GetServerSideProps } from "next";
3 | import { Session } from "next-auth/client";
4 | import { useState } from "react";
5 |
6 | import Button from "../components/Button";
7 | import privateRoute from "../lib/auth/privateRoute";
8 |
9 | interface ServerProps {
10 | initialPosts?: Array;
11 | }
12 |
13 | interface Props {
14 | session: Session;
15 | }
16 |
17 | const Crud = ({ initialPosts, session }: ServerProps & Props) => {
18 | const [value, setValue] = useState("");
19 | const [posts, setPosts] = useState(initialPosts || []);
20 | const [createLoading, setCreateLoading] = useState(false);
21 | const [deleteLoadingId, setDeleteLoadingId] = useState(-1);
22 |
23 | const handleChange = ({ target }: React.ChangeEvent) => {
24 | setValue(target.value);
25 | };
26 |
27 | const handleCreate = async () => {
28 | setCreateLoading(true);
29 |
30 | const response = await fetch("/api/post/create", {
31 | method: "POST",
32 | headers: {
33 | "Content-Type": "application/json",
34 | },
35 | body: JSON.stringify({ text: value }),
36 | });
37 |
38 | const json = await response.json();
39 |
40 | if (response.status === 200) {
41 | setPosts(concat([json], posts));
42 | } else {
43 | // handle error
44 | }
45 | setCreateLoading(false);
46 | setValue("");
47 | };
48 |
49 | const handleDelete = async (id: number) => {
50 | setDeleteLoadingId(id);
51 |
52 | await fetch(`/api/post/${id}/delete`, {
53 | method: "POST",
54 | headers: {
55 | "Content-Type": "application/json",
56 | },
57 | });
58 |
59 | setPosts(filter(posts, (post: any) => post.id != id));
60 |
61 | setDeleteLoadingId(-1);
62 | };
63 |
64 | return (
65 | <>
66 |
67 |
68 | You are {session ? "signed in" : "not signed in"}.
69 |
70 |
71 |
72 | This boilerplate includes{" "}
73 |
78 | Sequelize
79 | {" "}
80 | which is a Node.js ORM for Postgres, MySQL, MariaDB, SQLite and
81 | Microsoft SQL Server. This demo is connected to a Postgres instance.
82 |