A schedule management system with a React frontend and a Go backend.
- Backend: Go server with in-memory storage, HTTP API, and gRPC API
- Frontend: React + TypeScript app using React Query
- Concurrency handling and conflict prevention for bookings
backend/— Go backend (HTTP API, gRPC API, Storage)frontend/— React + TS frontend (Vite)
- Go 1.22+
- Node.js 18+ and npm
From the backend directory:
go run ./cmd/serverThis starts the gRPC server on port 8081 and the HTTP server on port 8080.
GRPC_ADDR: gRPC address to bind (default:8081)HTTP_ADDR: HTTP address to bind (default:8080)STORE_TYPE: Storage type (defaultpostgres)in-memory: In-memory storagepostgres: PostgreSQL storage
POSTGRES_URL: PostgreSQL database URL (defaultpostgres://root:@localhost:5432/grey_schedule?sslmode=disable)
Proto syntax: proto3
Package: schedule
Go package: schedulepb
Generator Command: protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/appointment.proto
Represents a single appointment.
| Field | Type | Description |
|---|---|---|
id |
string | Unique appointment ID |
title |
string | Appointment title |
start |
string | Start time (RFC3339) |
end |
string | End time (RFC3339) |
createdAt |
string | Creation timestamp (RFC3339) |
Request message for creating an appointment.
| Field | Type | Description |
|---|---|---|
title |
string | Appointment title |
start |
string | Start time (RFC3339) |
end |
string | End time (RFC3339) |
Response message for creating an appointment.
| Field | Type | Description |
|---|---|---|
appointment |
Appointment |
The created appointment |
Request message to list all appointments (empty message).
Response message for listing appointments.
| Field | Type | Description |
|---|---|---|
appointments |
repeated Appointment |
List of appointments |
Request message to delete an appointment.
| Field | Type | Description |
|---|---|---|
id |
string | ID of the appointment to delete |
Response message for deleting an appointment (empty message).
Defines the gRPC service for managing appointments.
| RPC Method | Request Type | Response Type | Description |
|---|---|---|---|
CreateAppointment |
CreateAppointmentRequest |
CreateAppointmentResponse |
Creates a new appointment |
ListAppointments |
ListAppointmentsRequest |
ListAppointmentsResponse |
Returns all appointments |
DeleteAppointment |
DeleteAppointmentRequest |
DeleteAppointmentResponse |
Deletes an appointment by ID |
Create an appointment
grpcurl -plaintext -d '{"title":"Team Meeting","start":"2025-10-14T17:00:00Z","end":"2025-10-14T18:00:00Z"}' localhost:8081 schedule.AppointmentService/CreateAppointmentList appointments
grpcurl -plaintext -d '{}' localhost:8081 schedule.AppointmentService/ListAppointmentsDelete an appointment
grpcurl -plaintext -d '{"id":"550e8400-e29b-41d4-a716-446655440000"}' localhost:8081 schedule.AppointmentService/DeleteAppointmentAll endpoints use JSON for request and response bodies.
GET /health
Returns the health status of the server.
Response:
- 200 OK:
ok
POST /api/appointments
Creates a new appointment. Validates the request and checks for time conflicts with existing appointments.
Request Body:
{
"title": "string",
"start": "2025-10-14T17:45:30Z",
"end": "2025-10-15T17:45:30Z"
}Request Fields:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Appointment title (1-255 characters) |
start |
string | Yes | Start time in RFC3339 format |
end |
string | Yes | End time in RFC3339 format (must be after start) |
Response:
-
201 Created
{ "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Team Meeting", "start": "2025-10-14T17:45:30Z", "end": "2025-10-15T17:45:30Z", "createdAt": "2025-10-14T16:00:00Z" } -
400 Bad Request
- Invalid JSON body
- Invalid RFC3339 timestamp format
- Title is empty or exceeds 255 characters
- Start time is not before end time
{ "error": "title is required and must be 1-255 characters" }{ "error": "invalid time range: start must be before end" }{ "error": "start and end must be RFC3339 timestamps" } -
409 Conflict
- Time conflict with existing appointment
{ "error": "time conflict with existing appointment" }
Example:
curl -X POST http://localhost:8080/api/appointments \
-H "Content-Type: application/json" \
-d '{
"title": "Team Meeting",
"start": "2025-10-14T17:00:00Z",
"end": "2025-10-14T18:00:00Z"
}'GET /api/appointments
Returns all appointments sorted by start time (and creation time if start times are equal).
Response:
- 200 OK
[ { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Team Meeting", "start": "2025-10-14T17:00:00Z", "end": "2025-10-14T18:00:00Z", "createdAt": "2025-10-14T16:00:00Z" }, { "id": "660f9511-f2ac-52e5-b827-557766551111", "title": "Client Call", "start": "2025-10-15T10:00:00Z", "end": "2025-10-15T11:00:00Z", "createdAt": "2025-10-14T16:30:00Z" } ]
Example:
curl http://localhost:8080/api/appointmentsDELETE /api/appointments/{id}
Deletes an appointment by ID.
URL Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | UUID of the appointment to delete |
Response:
-
204 No Content
- Appointment successfully deleted
-
404 Not Found
{ "error": "appointment not found" } -
500 Internal Server Error
{ "error": "internal error" }
Example:
curl -X DELETE http://localhost:8080/api/appointments/550e8400-e29b-41d4-a716-446655440000| Field | Type | Description |
|---|---|---|
id |
string | UUID of the appointment |
title |
string | Appointment title (1-255 characters) |
start |
string | Start time in RFC3339 format |
end |
string | End time in RFC3339 format |
createdAt |
string | Creation timestamp in RFC3339 format |
A Go unit test demonstrates safe concurrent booking:
go test ./internal/store -run TestInMemoryConcurrentCreateConflict -v # in-memory storage
go test ./internal/store -run TestPostgresConcurrentCreateConflict -v # PostgreSQL storageFrom the frontend directory:
npm install
npm run devYou should be able to access the app at http://localhost:5173.
VITE_API_BASE: base URL for the HTTP API (default http://localhost:8080)