Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
keynmol authored Nov 14, 2024
0 parents commit 2cb368c
Show file tree
Hide file tree
Showing 37 changed files with 3,459 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
**/.scala-build
**/.bsp
*.class
**/.bloop
**/.metals
.vscode
Dockerfile
fly.toml

32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI
on:
push:
branches: ["main"]
tags: ["v*"]
pull_request:
branches: ["*"]

jobs:
build:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: coursier/cache-action@v6
- uses: VirtusLab/scala-cli-setup@main
with:
power: true

- name: Check formatting
run: make code-check || echo "Run `make pre-ci`"

# Smithy4s is not idempotent unfortunately :(
# - name: Check generated code is up to date
# run: make smithy4s && git diff --exit-code

- name: Check Docker build
run: make docker

33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.class
.scala-build
.metals
.bsp
scalajs-frontend.js
*.semanticdb
db.json
*.map
6 changes: 6 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version = "3.8.1"
runner.dialect = scala3
rewrite.scala3.insertEndMarkerMinLines = 10
rewrite.scala3.removeOptionalBraces = true
rewrite.scala3.convertToNewSyntax = true

38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM node:22 as build

WORKDIR /usr/local/bin

RUN wget https://raw.githubusercontent.com/VirtusLab/scala-cli/main/scala-cli.sh && \
mv scala-cli.sh scala-cli && \
chmod +x scala-cli && \
scala-cli config power true && \
scala-cli version && \
echo '@main def hello = println(42)' | scala-cli run _ --js -S 3.5.0-RC2

WORKDIR /source
COPY shared shared

WORKDIR /source/frontend
COPY frontend/ .
RUN npm install && npm run build

WORKDIR /source/backend
COPY backend/ .
RUN scala-cli package . --assembly -f -o ./backend-assembly

FROM nginx

RUN apt update && apt install -y gpg wget && \
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null && \
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list && \
apt update && apt install -y temurin-22-jdk

COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY ./nginx/entrypoint.sh /app/entrypoint.sh
COPY --from=build /source/backend/backend-assembly /app/backend
COPY --from=build /source/frontend/dist /app/frontend

EXPOSE 80

CMD ["/app/entrypoint.sh"]

28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
docker:
docker build . -t my-fullstack-scala:latest

smithy4s:
cd shared && \
rm -rf fullstack_scala/protocol && \
cs launch smithy4s --contrib -- generate protocol.smithy --skip resource --skip openapi && \
scala-cli --power compile . -O -rewrite -O -source -O 3.4-migration

setup-ide:
rm -rf .scala-build .bsp .metals
cd shared && scala-cli --power setup-ide .
cd frontend && scala-cli --power setup-ide .
cd backend && scala-cli --power setup-ide .

code-check:
cd backend && scala-cli --power fmt . --check
cd frontend && scala-cli --power fmt . --check

pre-ci:
cd backend && scala-cli --power fmt .
cd frontend && scala-cli --power fmt .

run-backend:
cd backend && scala-cli --power run -w . --restart -- 9999

run-frontend:
cd frontend && npm run dev
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Full stack Scala 3

<!--toc:start-->
- [Full stack Scala 3](#full-stack-scala-3)
- [Pre-requisites](#pre-requisites)
- [Development workflow](#development-workflow)
- [Setting up build definitions for Metals](#setting-up-build-definitions-for-metals)
- [Updating protocol definitions](#updating-protocol-definitions)
- [Frontend live server](#frontend-live-server)
- [Backend live server](#backend-live-server)
- [Packaging as docker container](#packaging-as-docker-container)
<!--toc:end-->

**Frontend**: [Scala.js](https://www.scala-js.org/) and [Laminar](https://laminar.dev) | **Backend**: [Scala JVM](https://www.scala-lang.org/) and [Http4s](https://http4s.org/) | **Protocol**: [Smithy4s](https://disneystreaming.github.io/smithy4s/) | **Build system**: [Scala CLI](https://scala-cli.virtuslab.org/) and [Vite](https://vitejs.dev/) | **Packaging**: [Docker](https://hub.docker.com/) | **Web server**: [NGINX](https://nginx.org/)

This is an opinionated fullstack scala template:

- The frontend-backend interaction layer is handled through **Smithy4s** protocol definition – this ensures shared modelling and keeps all definitions in sync, additionally allowing for code sharing between frontend and backend
- **Scala CLI** is used everywhere. While it doesn't have multi-module support
- **NGINX** is used as a web server. Using a very configurable and battle-tested server from the very beginning can help implementing more complicated features later as the app grows. We also use it to serve static assets separately from the JVM backend.
- **Vite and TailwindCSS** are used on the frontend. Bundling a CSS framework can be helpful to get the styling off the ground for people not usually involved in frontend design work.

## Pre-requisites

1. **Frontend**: Node.js and NPM, Scala CLI
2. **Backend**: Scala CLI
3. **Protocol code**: [Coursier](https://get-coursier.io/docs/overview), Scala CLI

## Development workflow

### Setting up build definitions for Metals

Scala CLI doesn't have any support for multi-module projects, so to make sure we can work with this codebase in Metals we need to generate BSP definitions manually.

1. Run `make setup-ide` and then cross your fingers that Metals will pick everything up correctly

### Updating protocol definitions

1. Make changes to `shared/protocol.smithy`
2. Run `make smithy4s`
3. This will regenerate all the code that can be used by both backend and frontend

### Frontend live server

1. Run `make run-frontend`
2. Note that to perform any actions that execute API calls, you need to have backend running as well

### Backend live server

1. Run `make run-backend`
2. This will run the backend server on port 9999 – which is where Vite's frontend tooling expects it to be

### Packaging as docker container

1. Run `make docker`
2. Note that this project is organised as a self-contained docker image - just running `docker build .` will produce a working docker image. This is particularly useful for services like [Fly.io](https://fly.io/) which detect a Dockerfile and can build and deploy app directly from it
1 change: 1 addition & 0 deletions backend/.scalafmt.conf
38 changes: 38 additions & 0 deletions backend/backend.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fullstack_scala
package backend

import cats.data.Kleisli
import cats.effect.IO
import fullstack_scala.protocol.*
import org.http4s.HttpApp
import scribe.Scribe
import smithy4s.http4s.SimpleRestJsonBuilder
import cats.effect.std.Random
import cats.effect.Ref

def handleErrors(logger: Scribe[IO], routes: HttpApp[IO]): HttpApp[IO] =
import cats.syntax.all.*
routes.onError { exc =>
Kleisli(request => logger.error("Request failed", request.toString, exc))
}

class TestServiceImpl(ref: Ref[IO, List[Test]]) extends TestService[IO]:
override def listTests(): IO[ListTestsOutput] =
ref.get.map(ListTestsOutput(_))

override def createTest(attributes: TestAttributes) =
Random
.scalaUtilRandom[IO]
.flatMap(_.nextInt)
.map(id => Test(id = TestId(id), attributes = attributes))
.flatTap(test => ref.update(_ :+ test))
.map(CreateTestOutput(_))

end TestServiceImpl

def routesResource(service: TestService[IO]) =
import org.http4s.implicits.*
SimpleRestJsonBuilder
.routes(service)
.resource
.map(_.orNotFound)
9 changes: 9 additions & 0 deletions backend/project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//> using file ../shared/
//> using dep com.disneystreaming.smithy4s::smithy4s-http4s::0.18.23
//> using dep org.http4s::http4s-ember-server::0.23.27
//> using dep io.circe::circe-jawn::0.14.9
//> using dep com.outr::scribe-cats::3.15.0
//> using resourceDir ../frontend/dist
//> using scala 3.5.0-RC2
//> using option -experimental -Wunused:all

58 changes: 58 additions & 0 deletions backend/serverMain.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package fullstack_scala
package backend

import cats.effect.*
import com.comcast.ip4s.Port
import com.comcast.ip4s.host
import org.http4s.ember.server.EmberServerBuilder

import scala.concurrent.duration.*
import fullstack_scala.protocol.*

object Server extends IOApp:

override def run(args: List[String]) =
val port = args.headOption
.flatMap(_.toIntOption)
.flatMap(Port.fromInt)
.getOrElse(sys.error("port missing or invalid"))

val server =
for
ref <- IO.ref(DUMMY_DATA).toResource
routes <- routesResource(TestServiceImpl(ref))
server <- EmberServerBuilder
.default[IO]
.withPort(port)
.withHost(host"0.0.0.0")
.withHttpApp(handleErrors(scribe.cats.io, routes))
.withShutdownTimeout(0.seconds)
.build
.map(_.baseUri)
.evalTap(uri => IO.println(s"Server running on $uri"))
yield server

server.useForever
.as(ExitCode.Success)

end run

val DUMMY_DATA =
List(
Test(
TestId(1),
TestAttributes(
TestTitle("yass"),
description = Some(TestDescription("qween"))
)
),
Test(
TestId(2),
TestAttributes(
TestTitle("bless"),
description = None
)
)
)

end Server
1 change: 1 addition & 0 deletions frontend/.scalafmt.conf
Loading

0 comments on commit 2cb368c

Please sign in to comment.