├── .eslintrc.json ├── app ├── favicon.ico ├── layout.tsx ├── globals.css ├── api │ ├── (dashboard) │ │ └── notes │ │ │ ├── [note] │ │ │ └── route.ts │ │ │ └── route.ts │ └── (auth) │ │ └── users │ │ └── route.ts └── page.tsx ├── next.config.mjs ├── postcss.config.js ├── middlewares └── apis │ ├── loggingMiddleware.ts │ └── authMiddleware.ts ├── lib ├── modals │ ├── notes.ts │ └── user.ts └── db.ts ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── middleware.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umairjameel321/nextjs14-restapi/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /middlewares/apis/loggingMiddleware.ts: -------------------------------------------------------------------------------- 1 | export function loggingMiddleware(request: Request) { 2 | return { response: request.method + " " + request.url }; 3 | } 4 | -------------------------------------------------------------------------------- /lib/modals/notes.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const NoteSchema = new Schema({ 4 | title: { type: String, required: true }, 5 | description: { type: String }, 6 | user: { type: Schema.Types.ObjectId, ref: "User" }, 7 | }); 8 | 9 | const Note = models.Note || model("Note", NoteSchema); 10 | 11 | export default Note; 12 | -------------------------------------------------------------------------------- /lib/modals/user.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from "mongoose"; 2 | 3 | const UserSchema = new Schema({ 4 | email: { type: String, required: true, unique: true }, 5 | username: { type: String, required: true, unique: true }, 6 | password: { type: String, required: true }, 7 | }); 8 | 9 | const User = models.User || model("User", UserSchema); 10 | 11 | export default User; 12 | -------------------------------------------------------------------------------- /middlewares/apis/authMiddleware.ts: -------------------------------------------------------------------------------- 1 | const validateToken = (token: any) => { 2 | const validToken = true; 3 | if (!token || !validToken) { 4 | return false; 5 | } 6 | 7 | return true; 8 | }; 9 | 10 | export function authMiddleware(request: Request): any { 11 | const token = request.headers.get("authorization")?.split(" ")[1]; 12 | 13 | return { isValid: validateToken(token) }; 14 | } 15 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-apis", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "mongoose": "^8.1.1", 13 | "next": "14.1.0", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "autoprefixer": "^10.0.1", 22 | "eslint": "^8", 23 | "eslint-config-next": "14.1.0", 24 | "postcss": "^8", 25 | "tailwindcss": "^3.3.0", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { authMiddleware } from "./middlewares/apis/authMiddleware"; 3 | import { loggingMiddleware } from "./middlewares/apis/loggingMiddleware"; 4 | 5 | export const config = { 6 | matcher: "/api/:path*", 7 | }; 8 | 9 | export default function middleware(request: Request) { 10 | if (request.url.includes("/api/notes")) { 11 | const logResult = loggingMiddleware(request); 12 | console.log("Request", logResult.response); 13 | } 14 | 15 | const authResult = authMiddleware(request); 16 | if (!authResult?.isValid) { 17 | return new NextResponse(JSON.stringify({ message: "Unauthorized" }), { 18 | status: 401, 19 | }); 20 | } 21 | 22 | return NextResponse.next(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | 5 | const connect = async () => { 6 | const connectionState = mongoose.connection.readyState; 7 | 8 | if (connectionState === 1) { 9 | console.log("Already connected"); 10 | return; 11 | } 12 | 13 | if (connectionState === 2) { 14 | console.log("Connecting..."); 15 | return; 16 | } 17 | 18 | try { 19 | mongoose.connect(MONGODB_URI!, { 20 | dbName: "restapinext14", 21 | bufferCommands: false, 22 | }); 23 | console.log("Connected"); 24 | } catch (error) { 25 | console.log("Error in connecting to database", error); 26 | throw new Error("Error connecting to database"); 27 | } 28 | }; 29 | 30 | export default connect; 31 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /app/api/(dashboard)/notes/[note]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/lib/db"; 3 | import Note from "@/lib/modals/notes"; 4 | import { Types } from "mongoose"; 5 | import User from "@/lib/modals/user"; 6 | 7 | export const GET = async (request: Request, context: { params: any }) => { 8 | const noteId = context.params.note; 9 | try { 10 | const { searchParams } = new URL(request.url); 11 | const userId = searchParams.get("userId"); 12 | 13 | if (!userId || !Types.ObjectId.isValid(userId)) { 14 | return new NextResponse( 15 | JSON.stringify({ message: "Invalid or missing userId" }), 16 | { status: 400 } 17 | ); 18 | } 19 | 20 | if (!noteId || !Types.ObjectId.isValid(noteId)) { 21 | return new NextResponse( 22 | JSON.stringify({ message: "Invalid or missing note ID" }), 23 | { status: 400 } 24 | ); 25 | } 26 | 27 | await connect(); 28 | 29 | // Check if the user exists 30 | const user = await User.findById(userId); 31 | if (!user) { 32 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 33 | status: 404, 34 | }); 35 | } 36 | 37 | // Fetch the note and ensure it belongs to the user 38 | const note = await Note.findOne({ _id: noteId, user: userId }); 39 | if (!note) { 40 | return new NextResponse( 41 | JSON.stringify({ 42 | message: "Note not found or does not belong to the user", 43 | }), 44 | { status: 404 } 45 | ); 46 | } 47 | 48 | return new NextResponse(JSON.stringify(note), { status: 200 }); 49 | } catch (error) { 50 | return new NextResponse("Error in fetching note " + error, { 51 | status: 500, 52 | }); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /app/api/(auth)/users/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/lib/db"; 3 | import User from "@/lib/modals/user"; 4 | import { Types } from "mongoose"; 5 | 6 | const ObjectId = require("mongoose").Types.ObjectId; 7 | 8 | export const GET = async () => { 9 | try { 10 | await connect(); 11 | const users = await User.find(); 12 | return new NextResponse(JSON.stringify(users), { status: 200 }); 13 | } catch (error) { 14 | return new NextResponse("Error in fetching users" + error, { status: 500 }); 15 | } 16 | }; 17 | 18 | export const POST = async (request: Request) => { 19 | try { 20 | const body = await request.json(); 21 | 22 | await connect(); 23 | const newUser = new User(body); 24 | await newUser.save(); 25 | 26 | return new NextResponse( 27 | JSON.stringify({ message: "User is created", user: newUser }), 28 | { status: 201 } 29 | ); 30 | } catch (error) { 31 | return new NextResponse( 32 | JSON.stringify({ 33 | message: "Error in creating user", 34 | error, 35 | }), 36 | { 37 | status: 500, 38 | } 39 | ); 40 | } 41 | }; 42 | 43 | export const PATCH = async (request: Request) => { 44 | try { 45 | const body = await request.json(); 46 | const { userId, newUsername } = body; 47 | 48 | await connect(); 49 | 50 | if (!userId || !newUsername) { 51 | return new NextResponse( 52 | JSON.stringify({ message: "ID or new username are required" }), 53 | { 54 | status: 400, 55 | } 56 | ); 57 | } 58 | 59 | if (!Types.ObjectId.isValid(userId)) { 60 | return new NextResponse(JSON.stringify({ message: "Invalid userId" }), { 61 | status: 400, 62 | }); 63 | } 64 | 65 | const updatedUser = await User.findOneAndUpdate( 66 | { _id: new ObjectId(userId) }, 67 | { username: newUsername }, 68 | { new: true } 69 | ); 70 | 71 | if (!updatedUser) { 72 | return new NextResponse( 73 | JSON.stringify({ 74 | message: "User not found or didn't update user successfully.", 75 | }), 76 | { 77 | status: 400, 78 | } 79 | ); 80 | } 81 | 82 | // Return a success response 83 | return new NextResponse( 84 | JSON.stringify({ 85 | message: "Username updated successfully", 86 | user: updatedUser, 87 | }), 88 | { 89 | status: 200, 90 | } 91 | ); 92 | } catch (error) { 93 | return new NextResponse( 94 | JSON.stringify({ 95 | message: "Error updating username", 96 | error, 97 | }), 98 | { 99 | status: 500, 100 | } 101 | ); 102 | } 103 | }; 104 | 105 | export const DELETE = async (request: Request) => { 106 | try { 107 | const { searchParams } = new URL(request.url); 108 | const userId = searchParams.get("userId"); 109 | 110 | // Validate the userId 111 | if (!userId) { 112 | return new NextResponse( 113 | JSON.stringify({ message: "UserId is required" }), 114 | { 115 | status: 400, 116 | } 117 | ); 118 | } 119 | 120 | // Validate if userId is a valid ObjectId 121 | if (!Types.ObjectId.isValid(userId)) { 122 | return new NextResponse(JSON.stringify({ message: "Invalid userId" }), { 123 | status: 400, 124 | }); 125 | } 126 | 127 | await connect(); 128 | 129 | // TODO 130 | 131 | const deletedUser = await User.findByIdAndDelete( 132 | new Types.ObjectId(userId) 133 | ); 134 | 135 | // Check if the user was found and deleted 136 | if (!deletedUser) { 137 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 138 | status: 404, 139 | }); 140 | } 141 | 142 | // Return a success response 143 | return new NextResponse( 144 | JSON.stringify({ 145 | message: "User deleted successfully", 146 | }), 147 | { 148 | status: 200, 149 | } 150 | ); 151 | } catch (error) { 152 | return new NextResponse( 153 | JSON.stringify({ 154 | message: "Error deleting user", 155 | error, // Send a user-friendly error message 156 | }), 157 | { 158 | status: 500, 159 | } 160 | ); 161 | } 162 | }; 163 | -------------------------------------------------------------------------------- /app/api/(dashboard)/notes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import connect from "@/lib/db"; 3 | import Note from "@/lib/modals/notes"; 4 | import { Types } from "mongoose"; 5 | import User from "@/lib/modals/user"; 6 | 7 | export const GET = async (request: Request) => { 8 | try { 9 | const { searchParams } = new URL(request.url); 10 | const userId = searchParams.get("userId"); 11 | 12 | if (!userId || !Types.ObjectId.isValid(userId)) { 13 | return new NextResponse( 14 | JSON.stringify({ message: "Invalid or missing userId" }), 15 | { status: 400 } 16 | ); 17 | } 18 | 19 | await connect(); 20 | 21 | const user = await User.findById(userId); 22 | if (!user) { 23 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 24 | status: 404, 25 | }); 26 | } 27 | 28 | const notes = await Note.find({ user: new Types.ObjectId(userId) }); 29 | return new NextResponse(JSON.stringify(notes), { status: 200 }); 30 | } catch (error) { 31 | return new NextResponse("Error in fetching notes" + error, { status: 500 }); 32 | } 33 | }; 34 | 35 | export const POST = async (request: Request) => { 36 | try { 37 | const { searchParams } = new URL(request.url); 38 | const userId = searchParams.get("userId"); 39 | 40 | const body = await request.json(); 41 | const { title, description } = body; 42 | 43 | if (!userId || !Types.ObjectId.isValid(userId)) { 44 | return new NextResponse( 45 | JSON.stringify({ message: "Invalid or missing userId" }), 46 | { status: 400 } 47 | ); 48 | } 49 | 50 | await connect(); 51 | 52 | // Check if the user exists 53 | const user = await User.findById(userId); 54 | if (!user) { 55 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 56 | status: 404, 57 | }); 58 | } 59 | 60 | const newNote = new Note({ 61 | title, 62 | description, 63 | user: new Types.ObjectId(userId), 64 | }); 65 | 66 | await newNote.save(); 67 | return new NextResponse( 68 | JSON.stringify({ message: "Note created", note: newNote }), 69 | { status: 201 } 70 | ); 71 | } catch (error) { 72 | return new NextResponse( 73 | JSON.stringify({ 74 | message: "Error in creating note", 75 | error, 76 | }), 77 | { status: 500 } 78 | ); 79 | } 80 | }; 81 | 82 | export const PATCH = async (request: Request) => { 83 | try { 84 | const body = await request.json(); 85 | const { noteId, title, description } = body; 86 | 87 | const { searchParams } = new URL(request.url); 88 | const userId = searchParams.get("userId"); 89 | 90 | if (!noteId || !Types.ObjectId.isValid(noteId)) { 91 | return new NextResponse( 92 | JSON.stringify({ message: "Invalid or missing noteId" }), 93 | { status: 400 } 94 | ); 95 | } 96 | 97 | if (!userId || !Types.ObjectId.isValid(userId)) { 98 | return new NextResponse( 99 | JSON.stringify({ message: "Invalid or missing userId" }), 100 | { status: 400 } 101 | ); 102 | } 103 | 104 | await connect(); 105 | 106 | // Check if the user exists 107 | const user = await User.findById(userId); 108 | if (!user) { 109 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 110 | status: 404, 111 | }); 112 | } 113 | 114 | // Find the note and ensure it belongs to the user 115 | const note = await Note.findOne({ _id: noteId, user: userId }); 116 | if (!note) { 117 | return new NextResponse( 118 | JSON.stringify({ 119 | message: "Note not found or does not belong to the user", 120 | }), 121 | { 122 | status: 404, 123 | } 124 | ); 125 | } 126 | 127 | const updatedNote = await Note.findByIdAndUpdate( 128 | noteId, 129 | { title, description }, 130 | { new: true } 131 | ); 132 | 133 | return new NextResponse( 134 | JSON.stringify({ message: "Note updated", note: updatedNote }), 135 | { status: 200 } 136 | ); 137 | } catch (error) { 138 | return new NextResponse( 139 | JSON.stringify({ 140 | message: "Error in updating note", 141 | error, 142 | }), 143 | { status: 500 } 144 | ); 145 | } 146 | }; 147 | 148 | export const DELETE = async (request: Request) => { 149 | try { 150 | const { searchParams } = new URL(request.url); 151 | const noteId = searchParams.get("noteId"); 152 | const userId = searchParams.get("userId"); 153 | 154 | if (!userId || !Types.ObjectId.isValid(userId)) { 155 | return new NextResponse( 156 | JSON.stringify({ message: "Invalid or missing userId" }), 157 | { status: 400 } 158 | ); 159 | } 160 | 161 | if (!noteId || !Types.ObjectId.isValid(noteId)) { 162 | return new NextResponse( 163 | JSON.stringify({ message: "Invalid or missing noteId" }), 164 | { status: 400 } 165 | ); 166 | } 167 | 168 | await connect(); 169 | 170 | // Check if the user exists 171 | const user = await User.findById(userId); 172 | if (!user) { 173 | return new NextResponse(JSON.stringify({ message: "User not found" }), { 174 | status: 404, 175 | }); 176 | } 177 | 178 | // Check if the note exists and belongs to the user 179 | const note = await Note.findOne({ _id: noteId, user: userId }); 180 | if (!note) { 181 | return new NextResponse( 182 | JSON.stringify({ 183 | message: "Note not found or does not belong to the user", 184 | }), 185 | { 186 | status: 404, 187 | } 188 | ); 189 | } 190 | 191 | await Note.findByIdAndDelete(noteId); 192 | 193 | return new NextResponse( 194 | JSON.stringify({ message: "Note deleted successfully" }), 195 | { status: 200 } 196 | ); 197 | } catch (error) { 198 | return new NextResponse( 199 | JSON.stringify({ 200 | message: "Error in deleting note", 201 | error, 202 | }), 203 | { status: 500 } 204 | ); 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 |

8 | Get started by editing  9 | app/page.tsx 10 |

11 |
12 | 18 | By{" "} 19 | Vercel Logo 27 | 28 |
29 |
30 | 31 |
32 | Next.js Logo 40 |
41 | 42 |
43 | 49 |

50 | Docs{" "} 51 | 52 | -> 53 | 54 |

55 |

56 | Find in-depth information about Next.js features and API. 57 |

58 |
59 | 60 | 66 |

67 | Learn{" "} 68 | 69 | -> 70 | 71 |

72 |

73 | Learn about Next.js in an interactive course with quizzes! 74 |

75 |
76 | 77 | 83 |

84 | Templates{" "} 85 | 86 | -> 87 | 88 |

89 |

90 | Explore starter templates for Next.js. 91 |

92 |
93 | 94 | 100 |

101 | Deploy{" "} 102 | 103 | -> 104 | 105 |

106 |

107 | Instantly deploy your Next.js site to a shareable URL with Vercel. 108 |

109 |
110 |
111 |
112 | ); 113 | } 114 | --------------------------------------------------------------------------------