Template
Nuxt coursework boilerplate: Tailwind, Drizzle (MySQL), Redis, Pug page, Vercel deploy workflow
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Already set in your Coder workspace automatically - copy to .env only for
|
||||
# running outside Coder (e.g. testing locally against your own MySQL/Redis).
|
||||
MYSQL_HOST=school-mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=
|
||||
MYSQL_USER=
|
||||
MYSQL_PASSWORD=
|
||||
|
||||
REDIS_HOST=school-redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_USERNAME=
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# Only needed if you want the deploy workflow to push this project to Vercel.
|
||||
# Leave unset in Gitea Actions repo secrets and the deploy step is skipped.
|
||||
VERCEL_TOKEN=
|
||||
VERCEL_ORG_ID=
|
||||
VERCEL_PROJECT_ID=
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy-vercel:
|
||||
# Only runs once VERCEL_TOKEN is set as a repo secret - otherwise skipped.
|
||||
if: ${{ secrets.VERCEL_TOKEN != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Deploy to Vercel
|
||||
run: bunx vercel deploy --prod --yes --token "$VERCEL_TOKEN"
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,43 @@
|
||||
# Nuxt Boilerplate
|
||||
|
||||
Starter for the Nuxt coursework workspace: Bun + Nuxt + TypeScript + Tailwind,
|
||||
with MySQL (Drizzle ORM) and Redis wired up to the shared course databases.
|
||||
|
||||
## Development
|
||||
```bash
|
||||
bun install
|
||||
bun run db:push # creates the `notes` table from server/db/schema.ts
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 - the home page shows a small notes list (MySQL)
|
||||
and a key/value box (Redis) so you can see both connections working.
|
||||
|
||||
## What's here
|
||||
- `app/pages/index.vue` - example page using `<template lang="pug">`, calling
|
||||
the API routes below via `useFetch`/`$fetch`. Delete once you understand the
|
||||
pattern and start building your own pages.
|
||||
- `server/api/notes.get.ts` / `notes.post.ts` - example MySQL reads/writes via
|
||||
Drizzle.
|
||||
- `server/api/cache/[key].get.ts` / `[key].put.ts` - example Redis reads/writes.
|
||||
- `server/db/schema.ts` - your Drizzle table definitions. Add tables here, then
|
||||
run `bun run db:push` to sync them to MySQL (no migration files needed for
|
||||
coursework - `push` just diffs your schema against the live database).
|
||||
- `server/db/index.ts` / `server/utils/redis.ts` - pre-configured clients,
|
||||
reading connection details from environment variables. In your Coder
|
||||
workspace these are already set for you (`MYSQL_*`, `REDIS_*`) - you never
|
||||
need to type a password into code.
|
||||
- `.env.example` - only needed if you want to run this outside your Coder
|
||||
workspace, against your own local MySQL/Redis.
|
||||
- `.gitea/workflows/deploy.yml` - deploys to Vercel on push to `main`, but
|
||||
only if you've set `VERCEL_TOKEN` (and `VERCEL_ORG_ID`/`VERCEL_PROJECT_ID`)
|
||||
as repo secrets. Without them the job is skipped - nothing breaks.
|
||||
|
||||
## Notes
|
||||
- Your Redis user can only touch keys under `student:<your-username>:*`
|
||||
(enforced by the server) - that's why the cache routes prefix keys with
|
||||
`REDIS_USERNAME`. Keys outside that prefix will be rejected.
|
||||
- Your MySQL user only has access to your own database - no need to specify
|
||||
it beyond what's already in `MYSQL_DATABASE`.
|
||||
- Tailwind classes are available everywhere via `@nuxtjs/tailwindcss` - no
|
||||
extra CSS file needed for basic usage.
|
||||
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
const { data: notes, refresh } = await useFetch("/api/notes");
|
||||
|
||||
const title = ref("");
|
||||
const body = ref("");
|
||||
|
||||
async function addNote() {
|
||||
await $fetch("/api/notes", {
|
||||
method: "POST",
|
||||
body: { title: title.value, body: body.value },
|
||||
});
|
||||
title.value = "";
|
||||
body.value = "";
|
||||
await refresh();
|
||||
}
|
||||
|
||||
const cacheKey = ref("greeting");
|
||||
const cacheValue = ref("");
|
||||
const cacheResult = ref("");
|
||||
|
||||
async function saveCacheValue() {
|
||||
await $fetch(`/api/cache/${cacheKey.value}`, {
|
||||
method: "PUT",
|
||||
body: { value: cacheValue.value },
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCacheValue() {
|
||||
const res = await $fetch(`/api/cache/${cacheKey.value}`);
|
||||
cacheResult.value = res.value ?? "(nothing cached yet)";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
main.mx-auto.max-w-xl.p-8.space-y-8
|
||||
h1.text-2xl.font-bold Nuxt Boilerplate
|
||||
|
||||
section.space-y-2
|
||||
h2.text-lg.font-semibold Notes (MySQL via Drizzle)
|
||||
ul.list-disc.list-inside
|
||||
li(v-for="note in notes" :key="note.id") {{ note.title }} - {{ note.body }}
|
||||
form.flex.gap-2(@submit.prevent="addNote")
|
||||
input.border.rounded.px-2.py-1(v-model="title" placeholder="Title")
|
||||
input.border.rounded.px-2.py-1(v-model="body" placeholder="Body")
|
||||
button.bg-blue-600.text-white.px-3.py-1.rounded(type="submit") Add
|
||||
|
||||
section.space-y-2
|
||||
h2.text-lg.font-semibold Cache (Redis)
|
||||
.flex.gap-2
|
||||
input.border.rounded.px-2.py-1(v-model="cacheKey" placeholder="Key")
|
||||
input.border.rounded.px-2.py-1(v-model="cacheValue" placeholder="Value")
|
||||
button.bg-green-600.text-white.px-3.py-1.rounded(type="button" @click="saveCacheValue") Save
|
||||
button.bg-gray-600.text-white.px-3.py-1.rounded(type="button" @click="loadCacheValue") Load
|
||||
p {{ cacheResult }}
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./server/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "mysql",
|
||||
dbCredentials: {
|
||||
host: process.env.MYSQL_HOST ?? "localhost",
|
||||
port: Number(process.env.MYSQL_PORT ?? 3306),
|
||||
database: process.env.MYSQL_DATABASE!,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
modules: ['@nuxtjs/tailwindcss'],
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "nuxt-boilerplate",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"ioredis": "^6.0.0",
|
||||
"mysql2": "^3.24.3",
|
||||
"nuxt": "^4.5.2",
|
||||
"pug": "^3.0.4",
|
||||
"vue": "^3.5.42",
|
||||
"vue-router": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pug": "^2.0.10",
|
||||
"drizzle-kit": "^0.31.10"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { redis } from "~~/server/utils/redis";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const key = getRouterParam(event, "key");
|
||||
const value = await redis.get(`student:${process.env.REDIS_USERNAME}:${key}`);
|
||||
return { key, value };
|
||||
});
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { redis } from "~~/server/utils/redis";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const key = getRouterParam(event, "key");
|
||||
const body = await readBody<{ value: string }>(event);
|
||||
await redis.set(`student:${process.env.REDIS_USERNAME}:${key}`, body.value);
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { db } from "~~/server/db";
|
||||
import { notes } from "~~/server/db/schema";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return db.select().from(notes);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { db } from "~~/server/db";
|
||||
import { notes } from "~~/server/db/schema";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ title: string; body: string }>(event);
|
||||
await db.insert(notes).values({ title: body.title, body: body.body });
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export const db = drizzle({
|
||||
connection: {
|
||||
host: process.env.MYSQL_HOST ?? "localhost",
|
||||
port: Number(process.env.MYSQL_PORT ?? 3306),
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
},
|
||||
schema,
|
||||
mode: "default",
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { mysqlTable, int, varchar, timestamp } from "drizzle-orm/mysql-core";
|
||||
|
||||
export const notes = mysqlTable("notes", {
|
||||
id: int("id").primaryKey().autoincrement(),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
body: varchar("body", { length: 2000 }).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
export const redis = new Redis({
|
||||
host: process.env.REDIS_HOST ?? "localhost",
|
||||
port: Number(process.env.REDIS_PORT ?? 6379),
|
||||
username: process.env.REDIS_USERNAME,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
lazyConnect: true,
|
||||
maxRetriesPerRequest: 2,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user