create.handler.ts
import type { NextApiHandler } from 'next'
import bcrypt from 'bcrypt'
import { Prisma, User } from '@prisma/client'
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
import db from '@/lib/database'
interface Data {
error: string | null
data: User | null
}
const createHandler: NextApiHandler<Data> = async (req, res) => {
const { firstName, lastName, bio, username, password } =
req.body as Prisma.UserUncheckedCreateInput
if (
typeof username !== 'string' ||
typeof password !== 'string' ||
password.length < 6
) {
res
.status(StatusCodes.BAD_REQUEST)
.json({ data: null, error: ReasonPhrases.BAD_REQUEST })
return
}
const hashedPassword = bcrypt.hashSync(password, 10)
const data: Prisma.UserUncheckedCreateInput = {
username,
password: hashedPassword,
}
if (typeof firstName === 'string') {
data.firstName = firstName
}
if (typeof lastName === 'string') {
data.lastName = lastName
}
if (typeof bio === 'string') {
data.bio = bio
}
const createdUser = await db.user.create({ data })
res.status(StatusCodes.OK).json({ data: createdUser, error: null })
}
export default createHandler
delete.handler.ts
import type { NextApiHandler } from 'next'
import { User } from '@prisma/client'
import { StatusCodes } from 'http-status-codes'
import db from '@/lib/database'
interface Data {
data: User | null
error: string | null
}
const deleteHandler: NextApiHandler<Data> = async (req, res) => {
const id = typeof req.query.id === 'string' ? parseInt(req.query.id, 10) : 0
const user = await db.user.delete({ where: { id } })
res.status(StatusCodes.OK).json({ data: user, error: null })
}
export default deleteHandler
index.handler.ts
import type { NextApiHandler } from 'next'
import { User } from '@prisma/client'
import { StatusCodes } from 'http-status-codes'
import db from '@/lib/database'
interface Data {
data: User[]
}
const indexHandler: NextApiHandler<Data> = async (req, res) => {
const users = await db.user.findMany()
res.status(StatusCodes.OK).json({ data: users })
}
export default indexHandler
show.handler.ts
import type { NextApiHandler } from 'next'
import { User } from '@prisma/client'
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
import db from '@/lib/database'
interface Data {
data: User | null
error: string | null
}
const showHandler: NextApiHandler<Data> = async (req, res) => {
const id = typeof req.query.id === 'string' ? parseInt(req.query.id, 10) : 0
const user = await db.user.findFirst({ where: { id } })
if (!user) {
res
.status(StatusCodes.NOT_FOUND)
.json({ data: null, error: ReasonPhrases.NOT_FOUND })
}
res.status(StatusCodes.OK).json({ data: user, error: null })
}
export default showHandler
update.handler.ts
import type { NextApiHandler } from 'next'
import { Prisma, User } from '@prisma/client'
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
import db from '@/lib/database'
interface Data {
data: User | null
error: string | null
}
const updateHandler: NextApiHandler<Data> = async (req, res) => {
const id = typeof req.query.id === 'string' ? parseInt(req.query.id, 10) : 0
const { firstName, lastName, bio } =
req.body as Prisma.UserUncheckedUpdateInput
const data: Prisma.UserUncheckedUpdateInput = {}
if (typeof firstName === 'string') {
data.firstName = firstName
}
if (typeof lastName === 'string') {
data.lastName = lastName
}
if (typeof bio === 'string') {
data.bio = bio
}
const user = await db.user.update({ data, where: { id } })
res.status(StatusCodes.OK).json({ data: user, error: null })
}
export default updateHandler
Fonts.tsx
export default function Fonts() {
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
{
// eslint-disable-next-line @next/next/no-page-custom-font
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,600;0,700;0,800;1,600;1,700;1,800&family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
></link>
}
</>
);
}
ShowMessage.tsx
import { useStore } from '@/store/index'
const ShowMessage = () => {
const { helloWorld } = useStore()
return (
<div className="p-4 border border-gray-500">
Message: {helloWorld.message}
</div>
)
}
export default ShowMessage
make-handler.ts
import { Prisma } from '@prisma/client'
import type { NextApiHandler } from 'next'
import { ReasonPhrases, StatusCodes } from 'http-status-codes'
type Data = {
error: string
}
const makeHandler: (
handlerOptions: {
method: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE'
handler: NextApiHandler
}[],
) => NextApiHandler<Data> = (handlerOptions) => {
return async (req, res) => {
const handlerOption = handlerOptions.find(
(handlerOption) => handlerOption.method === req.method,
)
if (!handlerOption) {
res.setHeader(
'Allow',
handlerOptions.map((handlerOption) => handlerOption.method),
)
res
.status(StatusCodes.METHOD_NOT_ALLOWED)
.json({ error: ReasonPhrases.METHOD_NOT_ALLOWED })
return
}
try {
await handlerOption.handler(req, res)
} catch (e) {
console.error(e)
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
res.status(StatusCodes.BAD_REQUEST).json({
error: ReasonPhrases.BAD_REQUEST,
})
return
}
if (e.code === 'P2025') {
res.status(StatusCodes.NOT_FOUND).json({
error: ReasonPhrases.NOT_FOUND,
})
return
}
}
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: ReasonPhrases.INTERNAL_SERVER_ERROR,
})
}
}
}
export default makeHandler
database.ts
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma
hello-world.middleware.ts
import type { NextApiMiddleware } from '@/types/next'
const helloWorld: NextApiMiddleware = (handler) => {
return async (req, res) => {
if (
typeof req.cookies.sayHello === 'string' &&
req.cookies.sayHello === 'true'
) {
console.log('Hello, World!')
}
await handler(req, res)
}
}
export default helloWorld
[id].ts
import makeHandler from '@/core/make-handler'
import showHandler from '@/app/users/show.handler'
import updateHandler from '@/app/users/update.handler'
import deleteHandler from '@/app/users/delete.handler'
export default makeHandler([
{
method: 'GET',
handler: showHandler,
},
{
method: 'PUT',
handler: updateHandler,
},
{
method: 'DELETE',
handler: deleteHandler,
},
])
index.ts
import makeHandler from '@/core/make-handler'
import indexHandler from '@/app/users/index.handler'
import createHandler from '@/app/users/create.handler'
import helloWorld from '@/middlewares/hello-world.middleware'
export default makeHandler([
{
method: 'GET',
handler: helloWorld(indexHandler),
},
{
method: 'POST',
handler: createHandler,
},
])
hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: 'John Doe' })
}
_app.tsx
import '../styles/globals.scss'
import type { AppProps } from 'next/app'
import store, { StoreContext } from '@/store/index'
function MyApp({ Component, pageProps }: AppProps) {
return (
<StoreContext.Provider value={store}>
<Component {...pageProps} />
</StoreContext.Provider>
)
}
export default MyApp
_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
import Fonts from "../components/common/Google/Fonts";
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const originalRenderPage = ctx.renderPage;
// Run the React rendering logic synchronously
ctx.renderPage = async () => {
return originalRenderPage({
// Useful for wrapping the whole react tree
enhanceApp: (App) =>
function enhanceApp(props) {
return <App {...props} />;
},
// Useful for wrapping in a per-page basis
enhanceComponent: (Component) => Component,
});
};
// Run the parent `getInitialProps`, it now includes the custom `renderPage`
const initialProps = await Document.getInitialProps(ctx);
return initialProps;
}
render() {
return (
<Html>
<Head>
<meta charSet="utf-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<Fonts />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
index.tsx
import Head from 'next/head'
import type { NextPage } from 'next'
import { observer } from 'mobx-react-lite'
import { useStore } from '@/store/index'
import ShowMessage from '@/components/common/ShowMessage'
const Home: NextPage = () => {
const { helloWorld } = useStore()
return (
<>
<Head>
<title>Next.js Full-Stack Boilerplate</title>
<meta
name="description"
content="A Next.js full-stack boilerplate with TypeScript, Tailwind CSS, and Prisma.js for your future full-stack apps."
/>
</Head>
<main className="max-w-md mx-auto">
<h1 className="text-3xl font-bold font-open">
Next.js Full-Stack Boilerplate
</h1>
<p>
A Next.js full-stack boilerplate with TypeScript, Tailwind CSS, and
Prisma.js for your future full-stack apps.
</p>
<div className="mt-5 space-y-3">
<h2 className="text-xl font-bold">Say Hello</h2>
<input
className="w-full h-10 px-4 border border-gray-300 focus:outline-none focus:border-gray-400 focus:shadow"
value={helloWorld.message || ''}
onChange={(e) => helloWorld.setMessage(e.target.value)}
/>
<ShowMessage />
</div>
</main>
</>
)
}
export default observer(Home)
schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
firstName String @default("anonymous")
lastName String @default("anonymous")
bio String @default("")
username String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
seed.ts
import bcrypt from 'bcrypt'
import { Prisma, PrismaClient } from '@prisma/client'
const db = new PrismaClient()
async function seed() {
const usersCount = await db.user.count()
if (usersCount === 0) {
const users: Prisma.UserCreateManyInput[] = [
{
firstName: 'Leanne',
lastName: 'Graham',
bio: 'Pop culture fanatic. Freelance music lover. Unapologetic food fanatic. Bacon specialist.',
username: 'Bret',
password: bcrypt.hashSync(process.env.ADMIN_PASSWORD || 'admin', 10),
},
{
firstName: 'Ervin',
lastName: 'Howell',
bio: 'Friendly twitter practitioner. Bacon lover. Reader. General social media specialist. Student.',
username: 'Antonette',
password: bcrypt.hashSync(process.env.ADMIN_PASSWORD || 'admin', 10),
},
{
firstName: 'Clementine',
lastName: 'Bauch',
bio: 'Food lover. Twitter nerd. Internet evangelist. Alcohol enthusiast. Friendly explorer.',
username: 'Samantha',
password: bcrypt.hashSync(process.env.ADMIN_PASSWORD || 'admin', 10),
},
]
await db.user.createMany({
data: users,
})
}
}
seed()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await db.$disconnect()
})
hello-world.store.ts
import { makeAutoObservable } from 'mobx'
type Store = {
message: string
setMessage(message: string): void
}
const helloWorldStore = makeAutoObservable<Store>({
message: '',
setMessage(message) {
this.message = message
},
})
export default helloWorldStore
index.ts
import { createContext, useContext } from 'react'
import helloWorldStore from './hello-world.store'
const store = {
helloWorld: helloWorldStore,
}
export const StoreContext = createContext(store)
export const useStore = () => {
return useContext(StoreContext)
}
export default store
globals.scss
@tailwind base;
@tailwind components;
@tailwind utilities;global.d.ts
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
namespace NodeJS {
interface ProcessEnv {
ADMIN_PASSWORD: string | undefined
DATABASE_URL: string | undefined
}
}
}
next.d.ts
import type { NextApiHandler } from 'next'
export declare type NextApiMiddleware = (
handler: NextApiHandler,
) => NextApiHandler
.eslintrc.json
{
"extends": "next/core-web-vitals"
}
.prettierrc
{
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"semi": false
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
package.json
{
"name": "nextjs_full_stack_boilerplate",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:push": "prisma db push",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
"prisma:generate": "prisma generate"
},
"dependencies": {
"@prisma/client": "^4.0.0",
"bcrypt": "^5.0.1",
"http-status-codes": "^2.2.0",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"next": "12.2.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/node": "18.0.1",
"@types/react": "18.0.14",
"@types/react-dom": "18.0.5",
"autoprefixer": "^10.4.7",
"esbuild-register": "^3.3.3",
"eslint": "8.19.0",
"eslint-config-next": "12.2.0",
"postcss": "^8.4.14",
"prisma": "^4.0.0",
"sass": "^1.53.0",
"tailwindcss": "^3.1.6",
"typescript": "4.7.4"
}
}postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['Roboto', 'sans-serif'],
open: ['Open Sans', 'sans-serif'],
},
},
},
plugins: [],
}
tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/app/*": ["app/*"],
"@/components/*": ["components/*"],
"@/core/*": ["core/*"],
"@/hooks/*": ["hooks/*"],
"@/lib/*": ["lib/*"],
"@/middlewares/*": ["middlewares/*"],
"@/pages/*": ["pages/*"],
"@/store/*": ["store/*"],
"@/types/*": ["types/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}