A NextJS template with Prisma, variety of utility functions, and more.
- Install a postgresql
docker run --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres
- Optional: Install pgadmin:
docker run -p 5050:5050 -e 'PGADMIN_LISTEN_PORT=5050' -e '[email protected]' -e 'PGADMIN_DEFAULT_PASSWORD=admin' -d --name pgadmin4 dpage/pgadmin4
Then use 172.17.0.1:5432 to access database at host
- Create .env file which has got all the fields from
src/config.mjs
. If you want to disable some of the OAuth providers you can do it first. DATABASE_URL should be Prisma connection URL pnpm install
pnpm exec prisma db push
pnpm dev
- Go to localhost:3000 to see server running
- framework: Next.js
- orm: Prisma + postgres
- forms: react-hook-form
- validation: zod
- styles: tailwindcss, sass, normalize.css
- UI libraries: shadcn/ui
- icons: lucide-react
- linter: ESLint
- formatter: Prettier
- misc:
- axios
- react-datepicker
- @tanstack/react-table
- package.json
- authorization: lucia-auth
Familiarize yourself with how the existing OAuth methods work. On high level process goes like this.
-
Redirect user to
GET /login/:provider
-
This use OAuth provider adapter (preferably from
arctic
) to redirect user to Oauth2 provder consent screen. Remember to add email to scope. -
Provider should redirect user back to our service into
GET /login/:provider/callback
-
Validate input parameters and possible additional cookies set in
GET /login/:provider
-
Validate authorization code with provider adapter (preferably from
arctic
) -
Get user data from OAuth provider
-
Check if user email already exists in users table. If it does add new OAuth provider into existing account. If not, create a new user account
-
Finally create a new session
-
Redirect user back to
/
The config.mjs
file serves as a configuration module for your server environment. It utilizes the zod
library to enforce a schema for the environment variables, ensuring their correctness and type safety.
Open the config.mjs
. Within the z.object({...})
block, add a new key-value pair for your new environment variable. The key should be the name of the variable, and the value should be the validation type from zod
. For example, if you want to add a variable named NEW_VARIABLE
, and it's expected to be a string, you would add:
const schema = z.object({
NODE_ENV: z.string(),
DATABASE_URL: z.string(),
GITHUB_ID: z.string(),
GITHUB_SECRET: z.string(),
GOOGLE_ID: z.string(),
GOOGLE_SECRET: z.string(),
HOST: z.string(),
})
NEW_VARIABLE: z.string(),
After adding the variable to the schema, save the changes to config.mjs
. You can now access this new environment variable within your application code using the config
object exported from config.mjs
.
import config from './config.mjs'
const newVariableValue = config.NEW_VARIABLE
Open your .env
file and add a new line for the variable you've just added. Assign it an appropriate value according to its purpose. For example:
NEW_VARIABLE=my_value
To protect routes using the getCurrentUser
method, you can utilize it app / api routes. This ensures that only authenticated users can access certain routes. Here's how you can implement it:
import { getCurrentUser } from '@/lib/auth.ts'
// Example route / template
const routeOrPage = () => {
try {
const { user } = await getCurrentUser()
if (!user) {
// Redirect or throw
}
// Proceed with authenticated user
}
import { db } from '@/lib/db.ts'
import { getCurrentUser } from '@/lib/auth.ts'
import { handlePrismaError } from '@/lib/handle-prisma-error.ts'
const getUserSettings = async (userId: string) => {
return db.userSettings.findFirstOrThrow({ where: { id: userId } })
}
export default function UserData() {
const { user } = await getCurrentUser()
if (!user) {
redirect('/login')
}
const settings = await getUserSettings(user.id).catch(handlePrismaError) // returns 404 if entity was not found
return (
<div>...</div>
)
}
Use PrismaClient singleton. It's exported as db
from @/lib/db.ts
import { db } from `@/lib/db.ts`
(async () => {
// db is PrismaClient
const users = await db.users.findMany()
})()
It's recommended to wrap server actions with wrapServerAction
. This wraps the return into object containing success: true
and data. Additionally if you throw ServerActionError
it returns object with success: false
and corresonding error.
'server action'
import { wrapServerAction } from '@/lib/wrap-server-action'
import { ServerActionError } from '@/lib/server-action-error'
// in @/actions/example-action.ts
export const exampleAction = wrapServerAction(async (/* params */) => {
// put your server action here
if (/* some error */) {
throw new ServerActionError('Error with input')
}
})
import { useServerAction } from '@/lib/use-server-action'
export default function Page() {
const { action, loading, data, message } = useServerAction(serverAction)
return (
<div>
...
</div>
)
}
Component library is located at @/components
.
ButtonLoading
- shadcn Button with loading- OAuth specific login buttons
Menu
componentDateTimePicker
component
Additionally, shadcn/ui components are in subfolder @/components/ui
We use commitlint
to lint commit messages and enforce conventional commits. We highly recommend you to use squash merges. If your PR contains multiple fixes or features see this
Use Shadcn/ui typography with Typography
component. (Credits)
import { Typography } from '@/components/typography'
export const YourComponent = () => {
return (
...
<Typography variant="p" affects"muted">Muted paragraph</Typography>
...
)
}
- Lint-staged
- semantic-release OR release-please
- Logging