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