Uncommitted.DEV

Aleksi Kesälä’s devlog. A raw blog about building real software.

Work In Progress: Unclocked App WIP: Unclocked App

A web app for tracking and visualizing working hours or something like that.

- Built with the MERN stack.

Blog Posts

#0 Uncommitted Begins.

Welcome to Uncommitted.DEV — my personal devlog. I’ll be documenting the real work: building software, making decisions, and probably ranting about frameworks. This blog runs on raw HTML/CSS, zero frameworks, no bloat.

#1 Before the Blog: A Dev Recap Backend

Here’s what got done before the blog kicked off:

  • Project initialization with TypeScript, Express, and package management with Yarn.
  • Setting up MongoDB Atlas for cloud database & Mongoose for ODM. MongoDB model definitions.
  • Zod validators implemented for timeEntry CRUD operations.
  • Custom ZodHelper for ObjectId and Date coercion — enforces consistency across schema validations. (Might get back to this later.)
Key Observations
• Avoid use of as and any — sticking to type safety.
• Controllers hold the logic flow — services stay dumb.
• MongoDB ObjectId handled explicitly to keep schema clean.
• Pagination + filtering is typesafe and extensible.

#2 Setting Up Kanban Project Management

No-code Thursday — just one fancy Kanban board and a lot of brainstorming around the project.

Breaking down the backend into focused Kanban boards was today´s agenda, it should guide the implementation going forward, at least until I derail. Here’s a little sneak peek:

  • Auth - OAuth 2.0, JWT sessions & auth middleware
  • Time Entries - CRUD operations, filtering, and pagination
  • Client / Project - Schemas and full CRUD
  • Hourly Rates - Default & per-project overrides, auto earnings calculation(?)
  • Dashboard - Endpoints for hours, earnings, and chart data
  • Utils - Zod helpers, error handling middleware
  • Extra - Easter eggs, CSV export, maybe websockets?

Let’s not forget about the documentation.

Key Observations
• Writing things down helps keep chaos and scope creep off the charts.
• Kanban boards are solid for visualizing structure — some even give you an infinite canvas.
• I’m probably not sticking to this plan. Iterations will happen.

#3 Automate or Suffer: My git commits are now smarter than me DevEx

I’m lazy. I’m a programmer. So I automate.

A quick rundown of three DevEx upgrades I’ve added to this project: Docker, Yarn, and Husky. Docker's familiar, but I'm experimenting with Yarn for the first time here, and Husky is completely new to me. Early days, but the goal’s the same: automate the boring stuff, avoid mistakes, save time, and stop future me from rage-pushing broken commits.

Docker. I containerized the backend so I can spin it up the same way anywhere — no “works on my machine” nonsense. Basic setup: Dockerfile for the app, docker-compose.yml for dev setup with Mongo.

Yarn. Switched to yarn for consistency across machines and CI. Faster installs, zero excuses, and it’s been smooth so far. Locked down with .yarnrc.yml and yarn.lock in git.

Husky. Git hooks for grown-ups. Pre-commit runs prettier and eslint on changed files. If something fails, the commit dies — good.

Why?
• I’ll forget. Git won’t.
• Automate boring stuff. Save brain cycles for real problems.

#4 Long time no see, Backend! Backend

After a brief detour into DevEx tooling, I jumped back into the backend code and found my routes and handlers in disarray. To keep my sanity, I stripped out dead code, combined similar functionality, and reinforced RESTful naming conventions. Since I’m not ready to wrestle with OAuth, I threw together a simple fakeAuth middleware that injects an accountId onto req.

Refactoring: Spaghettimonster tore through the code; after resting my brain, I untangled the mess and moved on to the next side quest.

RESTful Principles: Side quest: I simplified my routes to GET /, POST / and DELETE /:id, making the API self-documenting.


// middleware/fakeAuth.ts
export const fakeAuth = (req, _res, next) => {
  req.accountId = req.headers["x-account-id"] || "{fakeAccountId-here}";
  next();
};
  

Implementing fakeAuth: This tiny middleware injects an accountId onto req. Simply mount it early: app.use(fakeAuth);

Pro tip: For any future auth implementation, mount fakeAuth as early as possible to prototype endpoints ASAP.

Key Observations
• Refactor your spaghetti, before it’s too late.
• RESTful routes make client integration straightforward.
fakeAuth injects accountId onto req, so GET / can fetch only that user’s entries—no need for an /accountId path param.
fakeAuth speeds up prototyping.
• Swapping in real OAuth later will be trivial once hooks are in place.

#5 CRUDs are in — now what? Backend

Basic CRUDs are in and working. Felt good to slow down and actually read the Mongoose docs for once — weirdly refreshing.

Next steps

Now it’s time to shape real business logic: tighten the scopes, reinforce integrity, and clean up architectural leftovers from the speed-run phase.

  • CRUD routes done
  • Business logic incoming
  • Frontend knocking on the door
Key Observations
• Two weeks with Copilot and already forgetting how to work without it.
• Might be a good time to write more — without Copilot.
• Back on Arch and Neovim. The basics. The simplicity. No distractions.

#6 Validation as the Source of Truth architecture

“Why define your data shape twice? With Zod, I define the schema once, and infer the TypeScript type straight from it.”

My favorite productivity hack in this project: the schema is the type. One definition, shared everywhere.

  • Validate incoming data at runtime with a single source of truth.
  • Use the same schemas for backend API validation and frontend type safety.
  • If I change a field, my editor instantly slaps out what needs fixing — no surprises.

No copy-paste. No out-of-date docs. The compiler and Zod keep me honest.

How it works in practice

import { dateString, objectId } from "@/../zodHelper";
import { z } from "zod";

const endTimeEntrySchema = z.object({
  params: z.object({
    id: objectId,
  }),
  body: z.object({
    endedAt: dateString,
  }),
});

// Type is always up-to-date with the schema:
export type EndTimeEntryInput = z.infer<typeof endTimeEntrySchema>;

Change the schema, and the type updates automatically. That's one source of truth for both validation and type safety.

Bonus: Helper Schemas

dateString and objectId are reusable Zod helpers for common patterns—so every date or MongoDB ObjectId in my API follows the same rules, everywhere.

The schema is the contract. The type is inferred. Validation is the documentation. No room for guessing.